55from typing import Literal
66
77from patchwork .common .tools .tool import Tool
8+ from patchwork .common .utils .utils import detect_newline
89
910
1011class CodeEditTool (Tool , tool_name = "code_edit_tool" ):
11- def __init__ (self , path : str ):
12+ def __init__ (self , path : Path ):
1213 super ().__init__ ()
13- self .repo_path = Path ( path )
14+ self .repo_path = path
1415 self .modified_files = set ()
1516
1617 @property
1718 def json_schema (self ) -> dict :
1819 return {
1920 "name" : "code_edit_tool" ,
20- "description" : """Custom editing tool for viewing, creating and editing files
21+ "description" : f"""\
22+ Custom editing tool for viewing, creating and editing files
2123
2224* State is persistent across command calls and discussions with the user
2325* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
2426* The `create` command cannot be used if the specified `path` already exists as a file
2527* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
2628* The `undo_edit` command will revert the last edit made to the file at `path`
29+ * The working directory is always { self .repo_path }
2730
2831Notes for using the `str_replace` command:
2932* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
@@ -86,40 +89,39 @@ def execute(
8689 return f"Error: `{ '` and `' .join (missing_required )} ` parameters must be set and cannot be empty"
8790
8891 try :
92+ abs_path = self .__get_abs_path (path )
8993 if command == "view" :
90- result = self .__view (path , view_range )
94+ result = self .__view (abs_path , view_range )
9195 elif command == "create" :
92- result = self .__create (file_text , path )
96+ result = self .__create (file_text , abs_path )
9397 elif command == "str_replace" :
94- result = self .__str_replace (new_str , old_str , path )
98+ result = self .__str_replace (new_str , old_str , abs_path )
9599 elif command == "insert" :
96- result = self .__insert (insert_line , new_str , path )
100+ result = self .__insert (insert_line , new_str , abs_path )
97101 else :
98102 return f"Error: Unknown action { command } "
99-
100- if command in {"create" , "str_replace" , "insert" }:
101- self .modified_files .update ({path .lstrip ("/" )})
102-
103- return result
104-
105103 except Exception as e :
106104 return f"Error: { str (e )} "
107105
106+ if command in {"create" , "str_replace" , "insert" }:
107+ self .modified_files .update ({abs_path .relative_to (self .repo_path )})
108+
109+ return result
110+
108111 @property
109112 def tool_records (self ):
110- return dict (modified_files = [{"path" : file } for file in self .modified_files ])
113+ return dict (modified_files = [{"path" : str ( file ) } for file in self .modified_files ])
111114
112115 def __get_abs_path (self , path : str ):
113- abs_path = (self .repo_path / path .lstrip ("/" )).resolve ()
114- if not abs_path .is_relative_to (self .repo_path .resolve ()):
116+ wanted_path = Path (path ).resolve ()
117+ if wanted_path .is_relative_to (self .repo_path ):
118+ return wanted_path
119+ else :
115120 raise ValueError (f"Path { path } contains illegal path traversal" )
116121
117- return abs_path
118-
119- def __view (self , path , view_range ):
120- abs_path = self .__get_abs_path (path )
122+ def __view (self , abs_path : Path , view_range ):
121123 if not abs_path .exists ():
122- return f"Error: Path { path } does not exist"
124+ return f"Error: Path { abs_path } does not exist"
123125
124126 if abs_path .is_file ():
125127 with open (abs_path , "r" ) as f :
@@ -141,38 +143,38 @@ def __view(self, path, view_range):
141143 result .append (f )
142144 return "\n " .join (result )
143145
144- def __create (self , file_text , path ):
145- abs_path = self .__get_abs_path (path )
146+ def __create (self , file_text , abs_path ):
146147 if abs_path .exists ():
147- return f"Error: File { path } already exists"
148+ return f"Error: File { abs_path } already exists"
148149 abs_path .parent .mkdir (parents = True , exist_ok = True )
149150 abs_path .write_text (file_text )
150- return f"File created successfully at: { path } "
151+ return f"File created successfully at: { abs_path } "
151152
152- def __str_replace (self , new_str , old_str , path ):
153- abs_path = self .__get_abs_path (path )
153+ def __str_replace (self , new_str , old_str , abs_path ):
154154 if not abs_path .exists ():
155- return f"Error: File { path } does not exist"
155+ return f"Error: File { abs_path } does not exist"
156156 if not abs_path .is_file ():
157- return f"Error: File { path } is not a file"
157+ return f"Error: File { abs_path } is not a file"
158158 content = abs_path .read_text ()
159159 occurrences = content .count (old_str )
160160 if occurrences != 1 :
161161 return f"Error: Found { occurrences } occurrences of old_str, expected exactly 1"
162162 new_content = content .replace (old_str , new_str )
163- with open (abs_path , "w" ) as f :
164- f .write (new_content )
163+ newline = detect_newline (abs_path )
164+ with abs_path .open ("w" , newline = newline ) as fp :
165+ fp .write (new_content )
165166 return "Replacement successful"
166167
167- def __insert (self , insert_line , new_str , path ):
168- abs_path = self .__get_abs_path (path )
168+ def __insert (self , insert_line , new_str , abs_path ):
169169 if not abs_path .is_file ():
170- return f"Error: File { path } does not exist or is not a file"
170+ return f"Error: File { abs_path } does not exist or is not a file"
171171
172172 lines = abs_path .read_text ().splitlines (keepends = True )
173173 if insert_line is None or insert_line < 1 or insert_line > len (lines ):
174174 return f"Error: Invalid insert line { insert_line } "
175175
176176 lines .insert (insert_line , new_str + "\n " )
177- abs_path .write_text ("" .join (lines ))
177+ newline = detect_newline (abs_path )
178+ with abs_path .open ("w" , newline = newline ) as fp :
179+ fp .write ("" .join (lines ))
178180 return "Insert successful"
0 commit comments