Module: Crimson::Tools::EditFile

Defined in:
lib/crimson/tools/edit_file.rb

Overview

Edit files by replacing strings, with single and multi-edit modes. Handles BOM, CRLF/LF line endings, and produces diffs.

Constant Summary collapse

TOOL_NAME =
"edit_file"
PARAMS =

Tool parameter definitions.

{
  path: { type: "string", description: "The path to the file to edit" },
  old_string: { type: "string", description: "The exact string to find and replace (single edit mode)" },
  new_string: { type: "string", description: "The string to replace it with (single edit mode)" },
  replace_all: { type: "boolean", description: "Replace all occurrences (default: false)" },
  edits: {
    type: "array",
    description: "Array of edits for multiple replacements in one call. Each edit has old_string, new_string, and optional replace_all.",
    items: {
      type: "object",
      properties: {
        old_string: { type: "string", description: "The exact string to find" },
        new_string: { type: "string", description: "The replacement string" },
        replace_all: { type: "boolean", description: "Replace all occurrences (default: false)" }
      },
      required: %w[old_string new_string]
    }
  }
}.freeze
MUTATION_QUEUE =
FileMutationQueue.new

Class Method Summary collapse

Class Method Details

.anthropic_definitionHash

Returns Anthropic-compatible tool definition.

Returns:

  • (Hash)

    Anthropic-compatible tool definition



48
49
50
# File 'lib/crimson/tools/edit_file.rb', line 48

def self.anthropic_definition
  Schema.build_anthropic(name: TOOL_NAME, description: "Replace strings in a file. Supports single edit or multiple edits in one call.", parameters: PARAMS, required: ["path"])
end

.call(path:, old_string: nil, new_string: nil, replace_all: false, edits: nil) ⇒ String

Execute the tool.

Parameters:

  • path (String)

    file path

  • old_string (String, nil) (defaults to: nil)

    text to find

  • new_string (String, nil) (defaults to: nil)

    replacement text

  • replace_all (Boolean) (defaults to: false)

    replace all occurrences

  • edits (Array<Hash>, nil) (defaults to: nil)

    multiple edits

Returns:

  • (String)

    result message with diff or error



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/crimson/tools/edit_file.rb', line 59

def self.call(path:, old_string: nil, new_string: nil, replace_all: false, edits: nil)
  return "Error: No path provided" if path.nil? || path.strip.empty?

  expanded = File.expand_path(path)

  MUTATION_QUEUE.with_file(expanded) do
    return "Error: File not found: #{path}" unless File.exist?(expanded)
    return "Error: Not a file: #{path}" unless File.file?(expanded)

    content = File.binread(expanded)
    has_bom = content.start_with?("\xEF\xBB\xBF")
    content = content.byteslice(3..) if has_bom
    content = content.force_encoding("UTF-8")

    line_ending = detect_line_ending(content)
    content = content.gsub("\r\n", "\n") if line_ending == :crlf

    old_content = content.dup

    if edits.is_a?(Array) && !edits.empty?
      count = 0
      edits.each do |e|
        result = apply_edit(content, e["old_string"], e["new_string"], e["replace_all"])
        return result[:error] if result[:error]
        content = result[:content]
        count += result[:count]
      end
    elsif old_string
      return "Error: No old_string provided" if old_string.nil? || old_string.empty?

      result = apply_edit(content, old_string, new_string, replace_all)
      return result[:error] if result[:error]

      content = result[:content]
      count = result[:count]
    else
      return "Error: Provide either old_string/new_string or edits array"
    end

    content = content.gsub("\n", "\r\n") if line_ending == :crlf
    content = "\xEF\xBB\xBF#{content}" if has_bom

    File.binwrite(expanded, content)

    clean_old = old_content
    clean_new = has_bom ? content.byteslice(3..) : content
    diff = DiffUtil.format_diff(clean_old, clean_new, path)
    "Successfully edited #{path} (#{count} replacement#{'s' if count != 1})\n#{diff}"
  end
rescue => e
  "Error editing file: #{e.message}"
end

.definitionHash

Returns OpenAI-compatible tool definition.

Returns:

  • (Hash)

    OpenAI-compatible tool definition



43
44
45
# File 'lib/crimson/tools/edit_file.rb', line 43

def self.definition
  Schema.build(name: TOOL_NAME, description: "Replace strings in a file. Supports single edit or multiple edits in one call.", parameters: PARAMS, required: ["path"])
end

.prepare_arguments(args) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



34
35
36
37
38
39
40
# File 'lib/crimson/tools/edit_file.rb', line 34

def self.prepare_arguments(args)
  if args["edits"].is_a?(Array)
    args["edits"].each { |e| e["replace_all"] = !!e["replace_all"] if e.key?("replace_all") }
  end
  args["replace_all"] = !!args["replace_all"] if args.key?("replace_all")
  args
end