replace-file-content-tool 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,156 @@
1
+ """
2
+ replace_file_content Tool
3
+ =========================
4
+ Edit a single contiguous block in an existing file by exact text match.
5
+
6
+ Parameters:
7
+ path - Absolute path to the file to edit (required).
8
+ target_content - The exact text to find and replace (required).
9
+ Must match character-for-character including whitespace.
10
+ replacement_content- The new text to substitute in place of target_content (required).
11
+ start_line - Start of the search range, 1-indexed (required).
12
+ end_line - End of the search range, 1-indexed inclusive (required).
13
+ allow_multiple - If true, all occurrences within the range are replaced.
14
+ If false (default), returns an error when multiple matches are found.
15
+
16
+ Use multi_replace_file_content when editing more than one non-adjacent block.
17
+ """
18
+
19
+ import logging
20
+ import os
21
+ from typing import Any, Dict
22
+ import aiofiles
23
+ from openchadpy.tool_base import ToolBase
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class Tool(ToolBase):
29
+ name = "replace_file_content"
30
+ description = (
31
+ "Edit a single contiguous block in an existing file. "
32
+ "Finds target_content within the line range [start_line, end_line] "
33
+ "and replaces it with replacement_content. "
34
+ "target_content must match exactly (including whitespace). "
35
+ "Use multi_replace_file_content for multiple non-adjacent edits."
36
+ )
37
+ input_schema = {
38
+ "type": "object",
39
+ "properties": {
40
+ "path": {
41
+ "type": "string",
42
+ "description": "Absolute path to the file to edit.",
43
+ },
44
+ "target_content": {
45
+ "type": "string",
46
+ "description": (
47
+ "The exact text to find and replace. "
48
+ "Must match character-for-character including whitespace."
49
+ ),
50
+ },
51
+ "replacement_content": {
52
+ "type": "string",
53
+ "description": "The new text to substitute in place of target_content.",
54
+ },
55
+ "start_line": {
56
+ "type": "integer",
57
+ "description": "Start of the search range, 1-indexed inclusive.",
58
+ },
59
+ "end_line": {
60
+ "type": "integer",
61
+ "description": "End of the search range, 1-indexed inclusive.",
62
+ },
63
+ "allow_multiple": {
64
+ "type": "boolean",
65
+ "description": (
66
+ "When true, all occurrences within the range are replaced. "
67
+ "When false (default), an error is returned if multiple matches are found."
68
+ ),
69
+ },
70
+ },
71
+ "required": [
72
+ "path",
73
+ "target_content",
74
+ "replacement_content",
75
+ "start_line",
76
+ "end_line",
77
+ ],
78
+ }
79
+ allowed_callers = ["direct", "code_execution"]
80
+
81
+ async def execute(self, **kwargs) -> Dict[str, Any]:
82
+ path: str = kwargs.get("path", "").strip()
83
+ target: str = kwargs.get("target_content", "")
84
+ replacement: str = kwargs.get("replacement_content", "")
85
+ start_line: int = int(kwargs.get("start_line", 1))
86
+ end_line: int = int(kwargs.get("end_line", 1))
87
+ allow_multiple: bool = bool(kwargs.get("allow_multiple", False))
88
+
89
+ if not path:
90
+ return {"error": "path is required and must not be empty."}
91
+ if not target:
92
+ return {"error": "target_content must not be empty."}
93
+ if not os.path.isfile(path):
94
+ return {"error": f"File not found: {path!r}"}
95
+ if start_line < 1 or end_line < start_line:
96
+ return {
97
+ "error": (
98
+ f"Invalid range: start_line={start_line}, end_line={end_line}. "
99
+ "Requires 1 <= start_line <= end_line."
100
+ )
101
+ }
102
+
103
+ try:
104
+ async with aiofiles.open(path, "r", encoding="utf-8", errors="replace") as f:
105
+ full_content = await f.read()
106
+ all_lines = full_content.splitlines(keepends=True)
107
+ except Exception as e:
108
+ return {"error": f"Failed to read file: {e}"}
109
+
110
+ total_lines = len(all_lines)
111
+ clamped_end = min(end_line, total_lines)
112
+
113
+ # Extract the slice text to search within
114
+ slice_text = "".join(all_lines[start_line - 1 : clamped_end])
115
+
116
+ count = slice_text.count(target)
117
+ if count == 0:
118
+ return {
119
+ "error": (
120
+ f"target_content not found within lines {start_line}–{clamped_end} "
121
+ f"of {path!r}."
122
+ )
123
+ }
124
+ if count > 1 and not allow_multiple:
125
+ return {
126
+ "error": (
127
+ f"Found {count} occurrences of target_content within lines "
128
+ f"{start_line}–{clamped_end}. Set allow_multiple=true to replace all."
129
+ )
130
+ }
131
+
132
+ if allow_multiple:
133
+ new_slice = slice_text.replace(target, replacement)
134
+ else:
135
+ new_slice = slice_text.replace(target, replacement, 1)
136
+
137
+ # Rebuild full file content
138
+ before = "".join(all_lines[: start_line - 1])
139
+ after = "".join(all_lines[clamped_end:])
140
+ new_content = before + new_slice + after
141
+
142
+ try:
143
+ async with aiofiles.open(path, "w", encoding="utf-8") as f:
144
+ await f.write(new_content)
145
+ except Exception as e:
146
+ return {"error": f"Failed to write file: {e}"}
147
+
148
+ logger.info(
149
+ f"[replace_file_content] replaced {count} occurrence(s) in {path!r} "
150
+ f"(lines {start_line}–{clamped_end})"
151
+ )
152
+ return {
153
+ "path": path,
154
+ "replacements_made": count if allow_multiple else 1,
155
+ "range": {"start_line": start_line, "end_line": clamped_end},
156
+ }
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: replace_file_content_tool
3
+ Version: 0.1.0
4
+ Summary: Edit a single contiguous block in an existing file.
5
+ Requires-Python: >=3.13
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: aiofiles>=25.1.0
8
+ Requires-Dist: openchadpy>=0.1.23
9
+ Requires-Dist: types-aiofiles>=25.1.0.20260518
@@ -0,0 +1,5 @@
1
+ replace_file_content_tool/main.py,sha256=hmUmgh-x_6WXY63ARFJbwEVSFTzGZ8c6WyIwwyW4IR0,5942
2
+ replace_file_content_tool-0.1.0.dist-info/METADATA,sha256=crLddLzyYLrXhfQ940SgB4-QNIEK6xfSx4LiYqIiPhw,307
3
+ replace_file_content_tool-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
4
+ replace_file_content_tool-0.1.0.dist-info/top_level.txt,sha256=kthnKtkJSvXvwP00iBFxEniP5Zs2ZH9rkiVibtBb4Vo,26
5
+ replace_file_content_tool-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ replace_file_content_tool