ayder-cli 2.2.8__tar.gz → 2.2.9__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.
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/PKG-INFO +1 -1
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/pyproject.toml +1 -1
- ayder_cli-2.2.9/src/ayder_cli/tools/builtins/filesystem.py +383 -0
- ayder_cli-2.2.9/src/ayder_cli/tools/builtins/search.py +533 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/search_definitions.py +20 -8
- ayder_cli-2.2.9/tests/tools/test_file_editor_hardening.py +155 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_impl.py +15 -7
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_impl_coverage.py +4 -2
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_search_codebase.py +3 -2
- ayder_cli-2.2.9/tests/tools/test_search_hardening.py +381 -0
- ayder_cli-2.2.8/src/ayder_cli/tools/builtins/filesystem.py +0 -306
- ayder_cli-2.2.8/src/ayder_cli/tools/builtins/search.py +0 -335
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/.github/workflows/ci.yml +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/.github/workflows/python-package.yml +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/.gitignore +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/.python-version +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/AGENTS.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/LICENSE +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/README.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/PROJECT_STRUCTURE.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/README_AGENT_WORKFLOW.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/README_OLLAMA.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/cc.png +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/config.toml.example +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/superpowers/plans/2026-06-26-file-editor-improvements.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/superpowers/plans/2026-06-26-session-save-resume.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/superpowers/specs/2026-06-26-file-editor-improvements-design.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/docs/superpowers/specs/2026-06-26-session-save-resume-design.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/__main__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/callbacks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/config.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/registry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/run.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/runner.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/agents/worktree.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/application/README.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/application/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/application/execution_policy.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/application/message_contract.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/application/runtime_factory.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/application/validation.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/cli.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/cli_callbacks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/cli_runner.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/console.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/cache_monitor.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/config.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/config_migration.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/context.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/context_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/context_manager_factory.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/default_context_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/ollama_context_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/result.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/core/session.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/logging_config.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/loops/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/loops/chat_loop.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/parser.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/process_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/prompts.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/base.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/claude.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/deepseek.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/gemini.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/glm.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/_errors.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/base.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/deepseek.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/generic_native.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/generic_xml.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/matrix.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/minimax.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/qwen3.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_drivers/registry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/ollama_inspector.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/openai.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/impl/qwen.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/orchestrator.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/providers/retry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/services/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/services/interactions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/themes/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/themes/ayder.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/themes/claude.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/background_process_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/context.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/context_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/filesystem_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/note_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/notes.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/notes_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/process_manager_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/shell.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/shell_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/skill.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/skill_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/task_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/task_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/tasks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/utils_tools.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/utils_tools_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/web.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/builtins/web_definitions.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/definition.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/execution.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/hooks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/normalization.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/plugin_api.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/plugin_github.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/plugin_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/plugin_status.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/registry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/schemas.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tools/utils.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/adapter.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/app.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/commands.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/helpers.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/keybindings.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/parser.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/rendering.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/screens.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/theme_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/types.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/tui/widgets.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/ui.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/src/ayder_cli/version.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/COVERAGE.md +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_agent_consolidated.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_agent_tools.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_callbacks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_config.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_integration.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_queue.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_registry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_run.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_runner.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_runner_provider_unavailable.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_worktree_helpers.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/agents/test_worktree_isolation.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/application/test_message_contract.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/application/test_runtime_factory.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/client/test_main.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/convergence/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/convergence/test_execution_policy_parity.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/convergence/test_runtime_wiring.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/convergence/test_validation_path.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_cache_monitor.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_config.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_config_coverage.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_config_migration.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_config_provider_logging.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_config_retry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_config_v2.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_context_manager_factory.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_default_context_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_default_context_manager_should_compact.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_ollama_context_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_ollama_context_manager_cache_monitor.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_ollama_context_manager_max_history.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_parameter_aliasing.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_parser.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/core/test_session.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/loops/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/loops/test_base.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/loops/test_chat_loop_hook.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/loops/test_chat_loop_tool_truncation.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_base.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_deepseek.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_errors.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_fallback.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_generic_native.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_generic_xml.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_matrix.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_minimax.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_provider_integration.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_qwen3.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/ollama_drivers/test_registry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_glm_async.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_ollama_inspector.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_ollama_native.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_ollama_xml.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_ollama_xml_autoroute.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_ollama_xml_usage.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_orchestrator_capabilities.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_qwen_async.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_retry_classifier.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_retry_integration.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/providers/test_retrying_provider.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/services/test_boundary.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/services/test_interaction_sink.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_cli.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_cli_callbacks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_context.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_env_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_logging_config.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_notes.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_process_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/test_tasks.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_agent_notes.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_background_process_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_bash_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_definition_discovery.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_di_injection.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_note_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_notes_api.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_path_security.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_plugin_api.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_plugin_github.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_plugin_loading.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_plugin_manager.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_plugin_status.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_registry.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_registry_coverage.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_result.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_schemas.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_skill_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_task_tool.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_utils.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tools/test_web.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/__init__.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_agent_list_screen.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_do_clear_coordination.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_follow_mode.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_help_screen.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_input_box.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_keybindings.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_multi_select_screen.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_multiline_input.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_paste.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_pending_compact_consumer.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_pull_delivery.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_resume.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_status_bar_badges.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_themes.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_turn_consumer.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/tui/test_widgets.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_confirm_screen.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_diff_preview.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_plugin_autoenable.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_provider_switch.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_run_tui_startup.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_assistant_sanitization.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_chat_loop.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_commands_agent.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_commands_logging.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_commands_plugin.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_commands_provider.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_commands_skill.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_helpers.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_temporal_command.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_tui_widgets.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_ui.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_ui_coverage.py +0 -0
- {ayder_cli-2.2.8 → ayder_cli-2.2.9}/tests/ui/test_ui_verbose.py +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem tools for ayder-cli.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import difflib
|
|
11
|
+
import tempfile
|
|
12
|
+
import threading
|
|
13
|
+
|
|
14
|
+
from ayder_cli.core.context import ProjectContext
|
|
15
|
+
from ayder_cli.core.result import ToolSuccess, ToolError
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Maximum file size allowed for read_file() to prevent DoS/memory exhaustion
|
|
20
|
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
21
|
+
|
|
22
|
+
# The chat loop executes auto-approved tool calls concurrently (asyncio.to_thread),
|
|
23
|
+
# so two edits targeting the same file race read-modify-write. Serialize per
|
|
24
|
+
# resolved path; edits to different files still run in parallel.
|
|
25
|
+
_PATH_LOCKS: dict[str, threading.Lock] = {}
|
|
26
|
+
_PATH_LOCKS_GUARD = threading.Lock()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _path_lock(abs_path) -> threading.Lock:
|
|
30
|
+
with _PATH_LOCKS_GUARD:
|
|
31
|
+
return _PATH_LOCKS.setdefault(str(abs_path), threading.Lock())
|
|
32
|
+
|
|
33
|
+
# Default page size for read_file when no explicit range is given. Mirrors
|
|
34
|
+
# Claude Code's Read tool — large enough for most source files in one shot,
|
|
35
|
+
# small enough that small-context LLMs don't drown.
|
|
36
|
+
READ_FILE_DEFAULT_LINES = 2000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _format_size(n_bytes: int) -> str:
|
|
40
|
+
if n_bytes < 1024:
|
|
41
|
+
return f"{n_bytes}B"
|
|
42
|
+
if n_bytes < 1024 * 1024:
|
|
43
|
+
return f"{n_bytes / 1024:.1f}KB"
|
|
44
|
+
return f"{n_bytes / (1024 * 1024):.1f}MB"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_file(
|
|
48
|
+
project_ctx: ProjectContext,
|
|
49
|
+
file_path: str,
|
|
50
|
+
start_line: int | None = None,
|
|
51
|
+
end_line: int | None = None,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Read a file with explicit pagination semantics.
|
|
54
|
+
|
|
55
|
+
Behavior:
|
|
56
|
+
- With no range: reads up to ``READ_FILE_DEFAULT_LINES`` starting at line 1.
|
|
57
|
+
If the file is longer, the result includes a footer naming the total
|
|
58
|
+
line count and the exact follow-up call needed to read the next page.
|
|
59
|
+
- With ``start_line`` / ``end_line``: reads that explicit slice (1-based,
|
|
60
|
+
inclusive). The same pagination footer is appended if more content
|
|
61
|
+
remains beyond the slice.
|
|
62
|
+
- The returned body always uses ``"<line_no>: <text>"`` formatting so the
|
|
63
|
+
caller can never confuse partial output for a complete file.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
abs_path = project_ctx.validate_path(file_path)
|
|
67
|
+
|
|
68
|
+
if not abs_path.exists():
|
|
69
|
+
rel_path = project_ctx.to_relative(abs_path)
|
|
70
|
+
return ToolError(f"Error: File '{rel_path}' does not exist.")
|
|
71
|
+
|
|
72
|
+
file_size = os.path.getsize(abs_path)
|
|
73
|
+
if file_size > MAX_FILE_SIZE:
|
|
74
|
+
rel_path = project_ctx.to_relative(abs_path)
|
|
75
|
+
return ToolError(
|
|
76
|
+
f"Error: File '{rel_path}' is too large "
|
|
77
|
+
f"({file_size / (1024 * 1024):.1f}MB). "
|
|
78
|
+
f"Maximum allowed size is {MAX_FILE_SIZE / (1024 * 1024):.0f}MB."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
82
|
+
lines = f.readlines()
|
|
83
|
+
|
|
84
|
+
total_lines = len(lines)
|
|
85
|
+
rel_path = str(project_ctx.to_relative(abs_path))
|
|
86
|
+
|
|
87
|
+
explicit_range = start_line is not None or end_line is not None
|
|
88
|
+
|
|
89
|
+
if explicit_range:
|
|
90
|
+
start = int(start_line) if start_line else 1
|
|
91
|
+
end = int(end_line) if end_line else total_lines
|
|
92
|
+
else:
|
|
93
|
+
start = 1
|
|
94
|
+
end = min(total_lines, READ_FILE_DEFAULT_LINES)
|
|
95
|
+
|
|
96
|
+
start_idx = max(0, start - 1)
|
|
97
|
+
end_idx = min(total_lines, end)
|
|
98
|
+
selected = lines[start_idx:end_idx]
|
|
99
|
+
|
|
100
|
+
body = "".join(f"{start + i}: {line}" for i, line in enumerate(selected))
|
|
101
|
+
|
|
102
|
+
# Always append a footer so the caller never has to infer completeness
|
|
103
|
+
# from absence. Two shapes:
|
|
104
|
+
# - more remaining → name the next call to make
|
|
105
|
+
# - reached EOF → explicit [COMPLETE] marker
|
|
106
|
+
last_returned = start_idx + len(selected)
|
|
107
|
+
more_remaining = last_returned < total_lines
|
|
108
|
+
|
|
109
|
+
if more_remaining:
|
|
110
|
+
footer = (
|
|
111
|
+
f"\n[FILE: {rel_path} | {total_lines} total lines, "
|
|
112
|
+
f"{_format_size(file_size)} | showing lines "
|
|
113
|
+
f"{start_idx + 1}-{last_returned}. "
|
|
114
|
+
f"To read more: read_file(file_path={rel_path!r}, "
|
|
115
|
+
f"start_line={last_returned + 1})]"
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
footer = (
|
|
119
|
+
f"\n[FILE: {rel_path} | COMPLETE: {total_lines} lines, "
|
|
120
|
+
f"{_format_size(file_size)} | showing lines "
|
|
121
|
+
f"{start_idx + 1}-{last_returned}]"
|
|
122
|
+
)
|
|
123
|
+
return ToolSuccess(body + footer)
|
|
124
|
+
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
return ToolError(f"Security Error: {str(e)}", "security")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
return ToolError(f"Error reading file: {str(e)}", "execution")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def file_explorer(project_ctx: ProjectContext, path: str = ".") -> str:
|
|
132
|
+
"""List files in a directory or get metadata for a specific file."""
|
|
133
|
+
try:
|
|
134
|
+
abs_path = project_ctx.validate_path(path)
|
|
135
|
+
|
|
136
|
+
if abs_path.is_dir():
|
|
137
|
+
files = [item.name for item in abs_path.iterdir()]
|
|
138
|
+
return ToolSuccess(json.dumps(files))
|
|
139
|
+
|
|
140
|
+
elif abs_path.exists():
|
|
141
|
+
rel_path = project_ctx.to_relative(abs_path)
|
|
142
|
+
stat = abs_path.stat()
|
|
143
|
+
size_bytes = stat.st_size
|
|
144
|
+
|
|
145
|
+
# Human-readable size
|
|
146
|
+
if size_bytes < 1024:
|
|
147
|
+
size_human = f"{size_bytes}B"
|
|
148
|
+
elif size_bytes < 1024 * 1024:
|
|
149
|
+
size_human = f"{size_bytes / 1024:.1f}KB"
|
|
150
|
+
else:
|
|
151
|
+
size_human = f"{size_bytes / (1024 * 1024):.1f}MB"
|
|
152
|
+
|
|
153
|
+
# Line count (only for files)
|
|
154
|
+
line_count = None
|
|
155
|
+
if abs_path.is_file():
|
|
156
|
+
try:
|
|
157
|
+
with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
|
|
158
|
+
line_count = sum(1 for _ in f)
|
|
159
|
+
except Exception:
|
|
160
|
+
line_count = None
|
|
161
|
+
|
|
162
|
+
info = {
|
|
163
|
+
"path": str(rel_path),
|
|
164
|
+
"size_bytes": size_bytes,
|
|
165
|
+
"size_human": size_human,
|
|
166
|
+
"line_count": line_count,
|
|
167
|
+
"extension": abs_path.suffix or None,
|
|
168
|
+
"is_file": abs_path.is_file(),
|
|
169
|
+
"is_directory": abs_path.is_dir(),
|
|
170
|
+
"is_symlink": abs_path.is_symlink(),
|
|
171
|
+
}
|
|
172
|
+
return ToolSuccess(json.dumps(info, indent=2))
|
|
173
|
+
else:
|
|
174
|
+
rel_path = project_ctx.to_relative(abs_path)
|
|
175
|
+
return ToolError(f"Error: '{rel_path}' does not exist.")
|
|
176
|
+
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
return ToolError(f"Security Error: {str(e)}", "security")
|
|
179
|
+
except Exception as e:
|
|
180
|
+
return ToolError(f"Error exploring path: {str(e)}", "execution")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _finalize(
|
|
184
|
+
abs_path,
|
|
185
|
+
rel_path,
|
|
186
|
+
old_content: str,
|
|
187
|
+
new_content: str,
|
|
188
|
+
success_msg: str,
|
|
189
|
+
dry_run: bool,
|
|
190
|
+
) -> str:
|
|
191
|
+
"""Write *new_content* and return *success_msg*, or, when *dry_run* is set,
|
|
192
|
+
return a unified diff and write nothing."""
|
|
193
|
+
if dry_run:
|
|
194
|
+
if old_content == new_content:
|
|
195
|
+
return ToolSuccess(f"[DRY RUN] No changes would be made to {rel_path}.")
|
|
196
|
+
diff = "".join(
|
|
197
|
+
difflib.unified_diff(
|
|
198
|
+
old_content.splitlines(keepends=True),
|
|
199
|
+
new_content.splitlines(keepends=True),
|
|
200
|
+
fromfile=f"a/{rel_path}",
|
|
201
|
+
tofile=f"b/{rel_path}",
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
return ToolSuccess(f"[DRY RUN] No changes written to {rel_path}.\n{diff}")
|
|
205
|
+
# Atomic write: a crash mid-write must never leave a truncated target,
|
|
206
|
+
# and concurrent readers must see either the old or the new content.
|
|
207
|
+
tmp_fd, tmp_name = tempfile.mkstemp(
|
|
208
|
+
dir=str(abs_path.parent), prefix=f".{abs_path.name}.", suffix=".tmp"
|
|
209
|
+
)
|
|
210
|
+
try:
|
|
211
|
+
with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
|
|
212
|
+
f.write(new_content)
|
|
213
|
+
if abs_path.exists():
|
|
214
|
+
os.chmod(tmp_name, os.stat(abs_path).st_mode)
|
|
215
|
+
os.replace(tmp_name, str(abs_path))
|
|
216
|
+
except BaseException:
|
|
217
|
+
with contextlib.suppress(OSError):
|
|
218
|
+
os.unlink(tmp_name)
|
|
219
|
+
raise
|
|
220
|
+
return ToolSuccess(success_msg)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_HINT_MIN_RATIO = 0.6
|
|
224
|
+
_HINT_MAX_FILE_CHARS = 1_000_000
|
|
225
|
+
_HINT_MAX_PREVIEW_CHARS = 300
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _closest_match_hint(file_content: str, old_string: str) -> str:
|
|
229
|
+
"""Locate the region most similar to a failed 'replace' target.
|
|
230
|
+
|
|
231
|
+
Returns a hint naming the line and a repr() of the region (so tabs,
|
|
232
|
+
trailing spaces, and other invisible mismatches become visible), or ''
|
|
233
|
+
when nothing is similar enough to be worth suggesting.
|
|
234
|
+
"""
|
|
235
|
+
if len(file_content) > _HINT_MAX_FILE_CHARS:
|
|
236
|
+
return ""
|
|
237
|
+
lines = file_content.splitlines()
|
|
238
|
+
if not lines:
|
|
239
|
+
return ""
|
|
240
|
+
window_size = max(1, len(old_string.splitlines()))
|
|
241
|
+
matcher = difflib.SequenceMatcher(autojunk=False)
|
|
242
|
+
matcher.set_seq2(old_string)
|
|
243
|
+
best_ratio = 0.0
|
|
244
|
+
best_line = 0
|
|
245
|
+
best_window = ""
|
|
246
|
+
for i in range(max(1, len(lines) - window_size + 1)):
|
|
247
|
+
window = "\n".join(lines[i : i + window_size])
|
|
248
|
+
matcher.set_seq1(window)
|
|
249
|
+
if (
|
|
250
|
+
matcher.real_quick_ratio() <= best_ratio
|
|
251
|
+
or matcher.quick_ratio() <= best_ratio
|
|
252
|
+
):
|
|
253
|
+
continue
|
|
254
|
+
ratio = matcher.ratio()
|
|
255
|
+
if ratio > best_ratio:
|
|
256
|
+
best_ratio, best_line, best_window = ratio, i + 1, window
|
|
257
|
+
if best_ratio < _HINT_MIN_RATIO:
|
|
258
|
+
return ""
|
|
259
|
+
preview = repr(best_window)
|
|
260
|
+
if len(preview) > _HINT_MAX_PREVIEW_CHARS:
|
|
261
|
+
preview = preview[:_HINT_MAX_PREVIEW_CHARS] + "…"
|
|
262
|
+
return (
|
|
263
|
+
f" Closest match at line {best_line} ({best_ratio:.0%} similar): "
|
|
264
|
+
f"{preview}. old_string must match the file byte-for-byte (repr shown: "
|
|
265
|
+
f"\\t = tab). Retry with the exact text, or dry_run=true to preview."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def file_editor(
|
|
270
|
+
project_ctx: ProjectContext,
|
|
271
|
+
file_path: str,
|
|
272
|
+
operation: str,
|
|
273
|
+
content: str | None = None,
|
|
274
|
+
old_string: str | None = None,
|
|
275
|
+
new_string: str | None = None,
|
|
276
|
+
line_number: int | None = None,
|
|
277
|
+
replace_all: bool = False,
|
|
278
|
+
regex: bool = False,
|
|
279
|
+
dry_run: bool = False,
|
|
280
|
+
) -> str:
|
|
281
|
+
"""Modify files with specific operations."""
|
|
282
|
+
try:
|
|
283
|
+
abs_path = project_ctx.validate_path(file_path)
|
|
284
|
+
rel_path = project_ctx.to_relative(abs_path)
|
|
285
|
+
|
|
286
|
+
with _path_lock(abs_path):
|
|
287
|
+
if operation == "write":
|
|
288
|
+
if content is None:
|
|
289
|
+
return ToolError("Error: 'content' parameter is required for 'write' operation.", "validation")
|
|
290
|
+
old_content = abs_path.read_text(encoding="utf-8") if abs_path.exists() else ""
|
|
291
|
+
if not dry_run:
|
|
292
|
+
abs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
293
|
+
return _finalize(
|
|
294
|
+
abs_path, rel_path, old_content, content,
|
|
295
|
+
f"Successfully wrote to {rel_path}", dry_run,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
elif operation == "replace":
|
|
299
|
+
if old_string is None or new_string is None:
|
|
300
|
+
return ToolError("Error: 'old_string' and 'new_string' are required for 'replace' operation.", "validation")
|
|
301
|
+
if not abs_path.exists():
|
|
302
|
+
return ToolError(f"Error: File '{rel_path}' does not exist.")
|
|
303
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
304
|
+
file_content = f.read()
|
|
305
|
+
if regex:
|
|
306
|
+
try:
|
|
307
|
+
pattern = re.compile(old_string)
|
|
308
|
+
except re.error as e:
|
|
309
|
+
return ToolError(f"Invalid regex pattern: {e}", "validation")
|
|
310
|
+
count = sum(1 for _ in pattern.finditer(file_content))
|
|
311
|
+
else:
|
|
312
|
+
count = file_content.count(old_string)
|
|
313
|
+
if count == 0:
|
|
314
|
+
hint = "" if regex else _closest_match_hint(file_content, old_string)
|
|
315
|
+
return ToolError(
|
|
316
|
+
f"Error: 'old_string' not found in {rel_path}. No changes made.{hint}"
|
|
317
|
+
)
|
|
318
|
+
if count > 1 and not replace_all:
|
|
319
|
+
return ToolError(
|
|
320
|
+
f"Error: 'old_string' is not unique (found {count} matches) in {rel_path}. "
|
|
321
|
+
f"Pass replace_all=true to replace all occurrences, or add surrounding "
|
|
322
|
+
f"context to make it unique. No changes made.",
|
|
323
|
+
"validation",
|
|
324
|
+
)
|
|
325
|
+
if regex:
|
|
326
|
+
new_content = pattern.sub(new_string, file_content, count=0 if replace_all else 1)
|
|
327
|
+
else:
|
|
328
|
+
new_content = (
|
|
329
|
+
file_content.replace(old_string, new_string)
|
|
330
|
+
if replace_all
|
|
331
|
+
else file_content.replace(old_string, new_string, 1)
|
|
332
|
+
)
|
|
333
|
+
noun = "occurrence" if count == 1 else "occurrences"
|
|
334
|
+
return _finalize(
|
|
335
|
+
abs_path, rel_path, file_content, new_content,
|
|
336
|
+
f"Successfully replaced {count} {noun} in {rel_path}", dry_run,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
elif operation == "insert":
|
|
340
|
+
if line_number is None or content is None:
|
|
341
|
+
return ToolError("Error: 'line_number' and 'content' are required for 'insert' operation.", "validation")
|
|
342
|
+
if not abs_path.exists():
|
|
343
|
+
return ToolError(f"Error: File '{rel_path}' does not exist.")
|
|
344
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
345
|
+
lines = f.readlines()
|
|
346
|
+
if line_number < 1:
|
|
347
|
+
return ToolError("Error: line_number must be >= 1.", "validation")
|
|
348
|
+
old_content = "".join(lines)
|
|
349
|
+
idx = min(line_number - 1, len(lines))
|
|
350
|
+
ins = content
|
|
351
|
+
if ins and not ins.endswith("\n"):
|
|
352
|
+
ins += "\n"
|
|
353
|
+
new_content = "".join(lines[:idx] + [ins] + lines[idx:])
|
|
354
|
+
return _finalize(
|
|
355
|
+
abs_path, rel_path, old_content, new_content,
|
|
356
|
+
f"Successfully inserted content at line {line_number} in {rel_path}", dry_run,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
elif operation == "delete":
|
|
360
|
+
if line_number is None:
|
|
361
|
+
return ToolError("Error: 'line_number' is required for 'delete' operation.", "validation")
|
|
362
|
+
if not abs_path.exists():
|
|
363
|
+
return ToolError(f"Error: File '{rel_path}' does not exist.")
|
|
364
|
+
with open(abs_path, "r", encoding="utf-8") as f:
|
|
365
|
+
lines = f.readlines()
|
|
366
|
+
if line_number < 1 or line_number > len(lines):
|
|
367
|
+
return ToolError(f"Error: line_number {line_number} is out of range (1-{len(lines)}).", "validation")
|
|
368
|
+
old_content = "".join(lines)
|
|
369
|
+
deleted = lines[line_number - 1]
|
|
370
|
+
new_content = "".join(lines[: line_number - 1] + lines[line_number:])
|
|
371
|
+
preview = deleted.rstrip("\n")[:80]
|
|
372
|
+
return _finalize(
|
|
373
|
+
abs_path, rel_path, old_content, new_content,
|
|
374
|
+
f"Deleted line {line_number} from {rel_path}: '{preview}'", dry_run,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
else:
|
|
378
|
+
return ToolError(f"Error: Unknown operation '{operation}'", "validation")
|
|
379
|
+
|
|
380
|
+
except ValueError as e:
|
|
381
|
+
return ToolError(f"Security Error: {str(e)}", "security")
|
|
382
|
+
except Exception as e:
|
|
383
|
+
return ToolError(f"Error executing file_editor: {str(e)}", "execution")
|