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.
Files changed (357) hide show
  1. {hud_python-0.5.35 → hud_python-0.5.36}/PKG-INFO +1 -1
  2. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_build.py +2 -2
  3. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/__init__.py +6 -0
  4. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/__init__.py +2 -0
  5. hud_python-0.5.36/hud/tools/coding/gemini_edit.py +340 -0
  6. hud_python-0.5.36/hud/tools/coding/gemini_write.py +92 -0
  7. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_gemini_tools.py +78 -15
  8. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/__init__.py +2 -0
  9. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/base.py +20 -4
  10. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/gemini.py +140 -44
  11. hud_python-0.5.36/hud/tools/filesystem/gemini_read_many.py +207 -0
  12. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_glob.py +13 -4
  13. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_grep.py +47 -1
  14. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_list.py +1 -1
  15. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/test_read.py +46 -8
  16. hud_python-0.5.36/hud/tools/filesystem/tests/test_read_many.py +121 -0
  17. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_version.py +1 -1
  18. {hud_python-0.5.35 → hud_python-0.5.36}/hud/version.py +1 -1
  19. {hud_python-0.5.35 → hud_python-0.5.36}/pyproject.toml +1 -1
  20. hud_python-0.5.35/hud/tools/coding/gemini_edit.py +0 -252
  21. {hud_python-0.5.35 → hud_python-0.5.36}/.gitignore +0 -0
  22. {hud_python-0.5.35 → hud_python-0.5.36}/LICENSE +0 -0
  23. {hud_python-0.5.35 → hud_python-0.5.36}/README.md +0 -0
  24. {hud_python-0.5.35 → hud_python-0.5.36}/examples/README.md +0 -0
  25. {hud_python-0.5.35 → hud_python-0.5.36}/hud/__init__.py +0 -0
  26. {hud_python-0.5.35 → hud_python-0.5.36}/hud/__main__.py +0 -0
  27. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/__init__.py +0 -0
  28. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/base.py +0 -0
  29. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/claude.py +0 -0
  30. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/gateway.py +0 -0
  31. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/gemini.py +0 -0
  32. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/gemini_cua.py +0 -0
  33. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/grounded_openai.py +0 -0
  34. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/misc/__init__.py +0 -0
  35. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/misc/integration_test_agent.py +0 -0
  36. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/misc/response_agent.py +0 -0
  37. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/openai.py +0 -0
  38. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/openai_chat.py +0 -0
  39. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/operator.py +0 -0
  40. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/resolver.py +0 -0
  41. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/__init__.py +0 -0
  42. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/conftest.py +0 -0
  43. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_base.py +0 -0
  44. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_base_runtime.py +0 -0
  45. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_claude.py +0 -0
  46. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_gemini.py +0 -0
  47. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_grounded_openai_agent.py +0 -0
  48. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_integration_test_agent.py +0 -0
  49. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_openai.py +0 -0
  50. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_operator.py +0 -0
  51. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_resolver.py +0 -0
  52. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/tests/test_run_eval.py +0 -0
  53. {hud_python-0.5.35 → hud_python-0.5.36}/hud/agents/types.py +0 -0
  54. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/__init__.py +0 -0
  55. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/__main__.py +0 -0
  56. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/analyze.py +0 -0
  57. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/build.py +0 -0
  58. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/cancel.py +0 -0
  59. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/__init__.py +0 -0
  60. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/base.py +0 -0
  61. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/harbor.py +0 -0
  62. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/tests/__init__.py +0 -0
  63. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/tests/conftest.py +0 -0
  64. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/convert/tests/test_harbor.py +0 -0
  65. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/debug.py +0 -0
  66. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/deploy.py +0 -0
  67. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/dev.py +0 -0
  68. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/eval.py +0 -0
  69. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/__init__.py +0 -0
  70. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/dev.py +0 -0
  71. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/init.py +0 -0
  72. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/tasks.py +0 -0
  73. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/templates.py +0 -0
  74. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/tests/__init__.py +0 -0
  75. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/flows/tests/test_dev.py +0 -0
  76. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/init.py +0 -0
  77. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/link.py +0 -0
  78. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/models.py +0 -0
  79. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/push.py +0 -0
  80. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/rl.py +0 -0
  81. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/scenario.py +0 -0
  82. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/sync.py +0 -0
  83. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/__init__.py +0 -0
  84. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analysis_utils.py +0 -0
  85. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analyze.py +0 -0
  86. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analyze_metadata.py +0 -0
  87. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_analyze_module.py +0 -0
  88. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_build_failure.py +0 -0
  89. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_build_module.py +0 -0
  90. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_init.py +0 -0
  91. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_main.py +0 -0
  92. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_more_wrappers.py +0 -0
  93. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_cli_root.py +0 -0
  94. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_convert.py +0 -0
  95. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_debug.py +0 -0
  96. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_debug_directory_mode.py +0 -0
  97. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_deploy.py +0 -0
  98. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_dev.py +0 -0
  99. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_eval.py +0 -0
  100. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_eval_bedrock.py +0 -0
  101. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_init.py +0 -0
  102. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_lockfile_utils.py +0 -0
  103. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_main_module.py +0 -0
  104. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_mcp_server.py +0 -0
  105. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_push.py +0 -0
  106. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_push_happy.py +0 -0
  107. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_push_wrapper.py +0 -0
  108. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_rl.py +0 -0
  109. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_scenario.py +0 -0
  110. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_sync.py +0 -0
  111. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/tests/test_utils.py +0 -0
  112. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/__init__.py +0 -0
  113. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/analysis.py +0 -0
  114. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/api.py +0 -0
  115. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/args.py +0 -0
  116. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/build_display.py +0 -0
  117. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/build_logs.py +0 -0
  118. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/collect.py +0 -0
  119. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/config.py +0 -0
  120. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/context.py +0 -0
  121. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/docker.py +0 -0
  122. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/env_check.py +0 -0
  123. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/environment.py +0 -0
  124. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/git.py +0 -0
  125. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/interactive.py +0 -0
  126. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/lockfile.py +0 -0
  127. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/logging.py +0 -0
  128. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/metadata.py +0 -0
  129. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/name_check.py +0 -0
  130. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/project_config.py +0 -0
  131. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/server.py +0 -0
  132. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/source_hash.py +0 -0
  133. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tasks.py +0 -0
  134. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/taskset.py +0 -0
  135. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/__init__.py +0 -0
  136. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_collect.py +0 -0
  137. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_config.py +0 -0
  138. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_docker.py +0 -0
  139. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_docker_hints.py +0 -0
  140. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_env_check.py +0 -0
  141. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_environment.py +0 -0
  142. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_git.py +0 -0
  143. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_interactive_module.py +0 -0
  144. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_logging_utils.py +0 -0
  145. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_metadata.py +0 -0
  146. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_source_hash.py +0 -0
  147. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/tests/test_tasks.py +0 -0
  148. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/validation.py +0 -0
  149. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/version_check.py +0 -0
  150. {hud_python-0.5.35 → hud_python-0.5.36}/hud/cli/utils/viewer.py +0 -0
  151. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/__init__.py +0 -0
  152. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/loader.py +0 -0
  153. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/runner.py +0 -0
  154. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/tests/__init__.py +0 -0
  155. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/tests/test_loader.py +0 -0
  156. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/tests/test_utils.py +0 -0
  157. {hud_python-0.5.35 → hud_python-0.5.36}/hud/datasets/utils.py +0 -0
  158. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/__init__.py +0 -0
  159. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connection.py +0 -0
  160. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/__init__.py +0 -0
  161. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/base.py +0 -0
  162. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/local.py +0 -0
  163. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/mcp_config.py +0 -0
  164. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/openai.py +0 -0
  165. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/connectors/remote.py +0 -0
  166. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/environment.py +0 -0
  167. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/__init__.py +0 -0
  168. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/adk.py +0 -0
  169. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/anthropic.py +0 -0
  170. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/gemini.py +0 -0
  171. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/langchain.py +0 -0
  172. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/llamaindex.py +0 -0
  173. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/integrations/openai.py +0 -0
  174. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/mock.py +0 -0
  175. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/router.py +0 -0
  176. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/scenarios.py +0 -0
  177. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/__init__.py +0 -0
  178. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_connection.py +0 -0
  179. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_connectors.py +0 -0
  180. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_environment.py +0 -0
  181. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_integrations.py +0 -0
  182. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_local_connectors.py +0 -0
  183. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_scenarios.py +0 -0
  184. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_session_id.py +0 -0
  185. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/tests/test_tools.py +0 -0
  186. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/types.py +0 -0
  187. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/__init__.py +0 -0
  188. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/formats.py +0 -0
  189. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/schema.py +0 -0
  190. {hud_python-0.5.35 → hud_python-0.5.36}/hud/environment/utils/tool_wrappers.py +0 -0
  191. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/__init__.py +0 -0
  192. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/context.py +0 -0
  193. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/display.py +0 -0
  194. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/instrument.py +0 -0
  195. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/manager.py +0 -0
  196. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/parallel.py +0 -0
  197. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/task.py +0 -0
  198. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/__init__.py +0 -0
  199. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_context.py +0 -0
  200. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_eval.py +0 -0
  201. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_manager.py +0 -0
  202. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_parallel.py +0 -0
  203. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/tests/test_task.py +0 -0
  204. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/types.py +0 -0
  205. {hud_python-0.5.35 → hud_python-0.5.36}/hud/eval/utils.py +0 -0
  206. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/__init__.py +0 -0
  207. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/chat.py +0 -0
  208. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/graders.py +0 -0
  209. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/permissions.py +0 -0
  210. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/skills.py +0 -0
  211. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/tests/__init__.py +0 -0
  212. {hud_python-0.5.35 → hud_python-0.5.36}/hud/native/tests/test_graders.py +0 -0
  213. {hud_python-0.5.35 → hud_python-0.5.36}/hud/patches/__init__.py +0 -0
  214. {hud_python-0.5.35 → hud_python-0.5.36}/hud/patches/mcp_patches.py +0 -0
  215. {hud_python-0.5.35 → hud_python-0.5.36}/hud/patches/warnings.py +0 -0
  216. {hud_python-0.5.35 → hud_python-0.5.36}/hud/py.typed +0 -0
  217. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/__init__.py +0 -0
  218. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/context.py +0 -0
  219. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/helper/__init__.py +0 -0
  220. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/low_level.py +0 -0
  221. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/router.py +0 -0
  222. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/server.py +0 -0
  223. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/__init__.py +0 -0
  224. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_add_tool.py +0 -0
  225. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_context.py +0 -0
  226. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_mcp_server_handlers.py +0 -0
  227. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_mcp_server_integration.py +0 -0
  228. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_mcp_server_more.py +0 -0
  229. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_prefix_naming.py +0 -0
  230. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_run_wrapper.py +0 -0
  231. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_server_extra.py +0 -0
  232. {hud_python-0.5.35 → hud_python-0.5.36}/hud/server/tests/test_sigterm_runner.py +0 -0
  233. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/__init__.py +0 -0
  234. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/chat.py +0 -0
  235. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/chat_service.py +0 -0
  236. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/reply_metadata.py +0 -0
  237. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/tests/__init__.py +0 -0
  238. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/tests/test_chat.py +0 -0
  239. {hud_python-0.5.35 → hud_python-0.5.36}/hud/services/tests/test_chat_service.py +0 -0
  240. {hud_python-0.5.35 → hud_python-0.5.36}/hud/settings.py +0 -0
  241. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/__init__.py +0 -0
  242. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/exceptions.py +0 -0
  243. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/hints.py +0 -0
  244. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/requests.py +0 -0
  245. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/__init__.py +0 -0
  246. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/test_exceptions.py +0 -0
  247. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/test_hints.py +0 -0
  248. {hud_python-0.5.35 → hud_python-0.5.36}/hud/shared/tests/test_requests.py +0 -0
  249. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/__init__.py +0 -0
  250. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/exporter.py +0 -0
  251. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/instrument.py +0 -0
  252. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/__init__.py +0 -0
  253. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/test_eval_telemetry.py +0 -0
  254. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/test_exporter.py +0 -0
  255. {hud_python-0.5.35 → hud_python-0.5.36}/hud/telemetry/tests/test_instrument.py +0 -0
  256. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/agent.py +0 -0
  257. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/base.py +0 -0
  258. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/apply_patch.py +0 -0
  259. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/bash.py +0 -0
  260. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/edit.py +0 -0
  261. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/gemini_shell.py +0 -0
  262. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/session.py +0 -0
  263. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/shell.py +0 -0
  264. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/__init__.py +0 -0
  265. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_apply_patch.py +0 -0
  266. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_bash.py +0 -0
  267. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_bash_extended.py +0 -0
  268. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_bash_integration.py +0 -0
  269. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_edit.py +0 -0
  270. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/tests/test_shell.py +0 -0
  271. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/coding/utils.py +0 -0
  272. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/__init__.py +0 -0
  273. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/anthropic.py +0 -0
  274. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/gemini.py +0 -0
  275. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/glm.py +0 -0
  276. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/hud.py +0 -0
  277. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/openai.py +0 -0
  278. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/qwen.py +0 -0
  279. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/settings.py +0 -0
  280. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/__init__.py +0 -0
  281. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_compression.py +0 -0
  282. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_computer.py +0 -0
  283. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_computer_actions.py +0 -0
  284. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/computer/tests/test_glm_computer.py +0 -0
  285. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/elicitation.py +0 -0
  286. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/__init__.py +0 -0
  287. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/base.py +0 -0
  288. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/pyautogui.py +0 -0
  289. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/tests/__init__.py +0 -0
  290. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/tests/test_base_executor.py +0 -0
  291. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/tests/test_pyautogui_executor.py +0 -0
  292. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/executors/xdo.py +0 -0
  293. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/glob.py +0 -0
  294. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/grep.py +0 -0
  295. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/list.py +0 -0
  296. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/read.py +0 -0
  297. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/filesystem/tests/__init__.py +0 -0
  298. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/__init__.py +0 -0
  299. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/config.py +0 -0
  300. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/grounded_tool.py +0 -0
  301. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/grounder.py +0 -0
  302. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/tests/__init__.py +0 -0
  303. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/grounding/tests/test_grounded_tool.py +0 -0
  304. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/__init__.py +0 -0
  305. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/base.py +0 -0
  306. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/code_execution.py +0 -0
  307. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/google_search.py +0 -0
  308. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/tool_search.py +0 -0
  309. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/url_context.py +0 -0
  310. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/web_fetch.py +0 -0
  311. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/hosted/web_search.py +0 -0
  312. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/jupyter.py +0 -0
  313. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/__init__.py +0 -0
  314. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/base.py +0 -0
  315. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/claude.py +0 -0
  316. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/gemini.py +0 -0
  317. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/session.py +0 -0
  318. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/__init__.py +0 -0
  319. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/test_claude.py +0 -0
  320. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/test_gemini.py +0 -0
  321. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/memory/tests/test_session.py +0 -0
  322. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/native_types.py +0 -0
  323. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/playwright.py +0 -0
  324. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/response.py +0 -0
  325. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/submit.py +0 -0
  326. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/__init__.py +0 -0
  327. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_agent_tool.py +0 -0
  328. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_base.py +0 -0
  329. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_elicitation.py +0 -0
  330. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_init.py +0 -0
  331. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_jupyter_tool.py +0 -0
  332. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_native_tool_e2e.py +0 -0
  333. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_native_types.py +0 -0
  334. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_playwright_tool.py +0 -0
  335. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_response.py +0 -0
  336. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_submit.py +0 -0
  337. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_tools.py +0 -0
  338. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_tools_init.py +0 -0
  339. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_types.py +0 -0
  340. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/tests/test_utils.py +0 -0
  341. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/types.py +0 -0
  342. {hud_python-0.5.35 → hud_python-0.5.36}/hud/tools/utils.py +0 -0
  343. {hud_python-0.5.35 → hud_python-0.5.36}/hud/types.py +0 -0
  344. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/__init__.py +0 -0
  345. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/env.py +0 -0
  346. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/hud_console.py +0 -0
  347. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/mcp.py +0 -0
  348. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/pretty_errors.py +0 -0
  349. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/serialization.py +0 -0
  350. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/strict_schema.py +0 -0
  351. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/__init__.py +0 -0
  352. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_init.py +0 -0
  353. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_pretty_errors.py +0 -0
  354. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_serialization.py +0 -0
  355. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tests/test_tool_shorthand.py +0 -0
  356. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/tool_shorthand.py +0 -0
  357. {hud_python-0.5.35 → hud_python-0.5.36}/hud/utils/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hud-python
3
- Version: 0.5.35
3
+ Version: 0.5.36
4
4
  Summary: SDK for the HUD platform.
5
5
  Project-URL: Homepage, https://github.com/hud-evals/hud-python
6
6
  Project-URL: Bug Tracker, https://github.com/hud-evals/hud-python/issues
@@ -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.35", "minor") == "0.6.0"
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.35", "major") == "1.0.0"
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 test_call_multiple_occurrences_no_expected(self) -> None:
156
- """Test call with multiple occurrences without expected_replacements replaces first only."""
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
- # Without expected_replacements, it replaces only the first occurrence
163
- result = await tool(
164
- file_path="test.txt",
165
- instruction="test edit",
166
- old_string="hello",
167
- new_string="world",
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 test_call_multiple_replacements(self) -> None:
197
- """Test multiple replacements with expected_replacements."""
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
- expected_replacements=3,
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",