104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
"""EditTool — replace exact string occurrences in a file.
|
|
|
|
Supports single (unique) replacement or replace-all mode.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any, ClassVar
|
|
|
|
from ..base import Tool, ToolParameter, ToolResult
|
|
|
|
|
|
class EditTool(Tool):
|
|
"""Replace old_string with new_string in a file.
|
|
|
|
By default, ``old_string`` must appear exactly once. Set ``replace_all``
|
|
to True to replace every occurrence.
|
|
"""
|
|
|
|
name: ClassVar[str] = "edit"
|
|
description: ClassVar[str] = (
|
|
"Perform exact string replacement in a file. "
|
|
"Fails if old_string is not found or is ambiguous (unless replace_all is True)."
|
|
)
|
|
parameters: ClassVar[list[ToolParameter]] = [
|
|
ToolParameter(
|
|
name="file_path",
|
|
type="string",
|
|
description="Absolute path to the file to edit",
|
|
),
|
|
ToolParameter(
|
|
name="old_string",
|
|
type="string",
|
|
description="The exact text to find and replace",
|
|
),
|
|
ToolParameter(
|
|
name="new_string",
|
|
type="string",
|
|
description="The replacement text",
|
|
),
|
|
ToolParameter(
|
|
name="replace_all",
|
|
type="boolean",
|
|
description="Replace all occurrences instead of requiring uniqueness",
|
|
required=False,
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
file_path = Path(kwargs["file_path"])
|
|
old_string: str = kwargs["old_string"]
|
|
new_string: str = kwargs["new_string"]
|
|
replace_all: bool = kwargs.get("replace_all", False)
|
|
|
|
if not file_path.is_absolute():
|
|
return ToolResult.fail(f"Path must be absolute: {file_path}")
|
|
|
|
if not file_path.exists():
|
|
return ToolResult.fail(f"File not found: {file_path}")
|
|
|
|
if not file_path.is_file():
|
|
return ToolResult.fail(f"Not a file: {file_path}")
|
|
|
|
if old_string == new_string:
|
|
return ToolResult.fail("old_string and new_string are identical")
|
|
|
|
try:
|
|
content = file_path.read_text(encoding="utf-8")
|
|
except UnicodeDecodeError:
|
|
return ToolResult.fail(f"Cannot read binary file: {file_path}")
|
|
except PermissionError:
|
|
return ToolResult.fail(f"Permission denied: {file_path}")
|
|
|
|
occurrence_count = content.count(old_string)
|
|
|
|
if occurrence_count == 0:
|
|
return ToolResult.fail(
|
|
f"old_string not found in {file_path}. "
|
|
"Verify the exact text including whitespace and indentation."
|
|
)
|
|
|
|
if not replace_all and occurrence_count > 1:
|
|
return ToolResult.fail(
|
|
f"old_string found {occurrence_count} times in {file_path}. "
|
|
"Provide more context to make it unique, or set replace_all=True."
|
|
)
|
|
|
|
if replace_all:
|
|
new_content = content.replace(old_string, new_string)
|
|
else:
|
|
new_content = content.replace(old_string, new_string, 1)
|
|
|
|
try:
|
|
file_path.write_text(new_content, encoding="utf-8")
|
|
except PermissionError:
|
|
return ToolResult.fail(f"Permission denied writing to: {file_path}")
|
|
|
|
return ToolResult.success(
|
|
f"Replaced {occurrence_count} occurrence(s) in {file_path}",
|
|
file_path=str(file_path),
|
|
replacements=occurrence_count,
|
|
)
|