# frozen_string_literal: truemoduleClackymoduleToolsclassEdit<Baseself.tool_name="edit"self.tool_description="Make precise edits to existing files by replacing old text with new text. "\"The old_string must match exactly (including whitespace and indentation)."self.tool_category="file_system"self.tool_parameters={type: "object",properties: {path: {type: "string",description: "The path of the file to edit (absolute or relative)"},old_string: {type: "string",description: "The exact string to find and replace (must match exactly including whitespace)"},new_string: {type: "string",description: "The new string to replace the old string with"},replace_all: {type: "boolean",description: "If true, replace all occurrences. If false (default), replace only the first occurrence",default: false}},required: %w[path old_string new_string]}defexecute(path:,old_string:,new_string:,replace_all: false,working_dir: nil)# Expand ~ to home directory, resolve relative paths against working_dirpath=expand_path(path,working_dir: working_dir)unlessFile.exist?(path)return{error: "File not found: #{path}"}endunlessFile.file?(path)return{error: "Path is not a file: #{path}"}endbegin# Scrub invalid UTF-8 bytes at read time — otherwise editing a file# that contains non-UTF-8 bytes would poison history / error messages# and cause JSON.generate to fail during replay.content=safe_utf8(File.read(path))# Find matching string using layered strategy (shared with preview)match_result=Utils::StringMatcher.find_match(content,old_string)unlessmatch_resultreturnbuild_helpful_error(content,old_string,path)endactual_old_string=match_result[:matched_string]occurrences=match_result[:occurrences]# If not replace_all and multiple occurrences, warn about ambiguityif!replace_all&&occurrences>1return{error: "String appears #{occurrences} times in the file. Use replace_all: true to replace all occurrences, "\"or provide a more specific string that appears only once."}end# Perform replacementcontent=replace_all?content.gsub(actual_old_string,new_string):content.sub(actual_old_string,new_string)File.write(path,content){path: File.expand_path(path),replacements: replace_all?occurrences:1,error: nil}rescueErrno::EACCES=>e{error: "Permission denied: #{e.message}"}rescueStandardError=>e{error: "Failed to edit file: #{e.message}"}endendprivatedefbuild_helpful_error(content,old_string,path)old_lines=old_string.linesfirst_line_pattern=old_lines.first&.stripiffirst_line_pattern&&!first_line_pattern.empty?content_lines=content.linessimilar_locations=[]content_lines.each_with_indexdo|line,idx|ifline.strip==first_line_patternstart_idx=[0,idx-2].maxend_idx=[content_lines.length-1,idx+old_lines.length+2].mincontext=content_lines[start_idx..end_idx].joinsimilar_locations<<{line_number: idx+1,context: context}endendifsimilar_locations.any?context_display=similar_locations.first[:context].lines.first(5).map{|l|" #{l}"}.joinreturn{error: "String to replace not found in file. The first line of old_string exists at line #{similar_locations.first[:line_number]}, "\"but the full multi-line string doesn't match. This is often caused by whitespace differences (tabs vs spaces). "\"\n\nContext around line #{similar_locations.first[:line_number]}:\n#{context_display}\n\n"\"TIP: Use file_reader to see the actual content, then retry. No need to explain, just execute the tools."}endend{error: "String to replace not found in file '#{File.basename(path)}'. "\"Make sure old_string matches exactly (including all whitespace). "\"TIP: Use file_reader to view the exact content first, then retry. No need to explain, just execute the tools."}enddefformat_call(args)path=args[:file_path]||args["file_path"]||args[:path]||args["path"]"Edit(#{Utils::PathHelper.safe_basename(path)})"enddefformat_result(result)returnresult[:error]ifresult[:error]replacements=result[:replacements]||result["replacements"]||1"Modified #{replacements} occurrence#{replacements>1?"s":""}"end# Scrub invalid UTF-8 byte sequences (see file_reader.rb for rationale).privatedefsafe_utf8(str)returnstrifstr.nil?returnstrifstr.encoding==Encoding::UTF_8&&str.valid_encoding?str.encode("UTF-8",invalid: :replace,undef: :replace,replace: "\u{FFFD}")endendendend