summaryrefslogtreecommitdiffhomepage
path: root/lib/dispatch/tool/files/edit_file.rb
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-03-31 23:10:45 +0900
committerAdam Malczewski <[email protected]>2026-03-31 23:10:45 +0900
commit57c56daf5906442dacc15951c9b3405f89309839 (patch)
treee76890119c0e47acb48f8585222b7a2f5e22df56 /lib/dispatch/tool/files/edit_file.rb
parent25488d32336e05b69a41391cc7b5153478d3cc8a (diff)
downloaddispatch-tool-files-dev.tar.gz
dispatch-tool-files-dev.zip
impdev
Diffstat (limited to 'lib/dispatch/tool/files/edit_file.rb')
-rw-r--r--lib/dispatch/tool/files/edit_file.rb91
1 files changed, 91 insertions, 0 deletions
diff --git a/lib/dispatch/tool/files/edit_file.rb b/lib/dispatch/tool/files/edit_file.rb
new file mode 100644
index 0000000..b1bd159
--- /dev/null
+++ b/lib/dispatch/tool/files/edit_file.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Dispatch
+ module Tool
+ module Files
+ EDIT_FILE = Dispatch::Tools::Definition.new(
+ name: "edit_file",
+ description: "Apply text edits to an existing file. Each edit replaces old_text with new_text sequentially.",
+ parameters: {
+ type: "object",
+ properties: {
+ path: {
+ type: "string",
+ description: "Path to the file to edit, relative to the worktree root."
+ },
+ edits: {
+ type: "array",
+ description: "Array of edit operations to apply sequentially.",
+ items: {
+ type: "object",
+ properties: {
+ old_text: {
+ type: "string",
+ description: "The exact text to find in the file."
+ },
+ new_text: {
+ type: "string",
+ description: "The text to replace old_text with."
+ }
+ },
+ required: %w[old_text new_text],
+ additionalProperties: false
+ }
+ }
+ },
+ required: %w[path edits],
+ additionalProperties: false
+ }
+ ) do |params, context|
+ worktree_path = context[:worktree_path]
+ path = params[:path]
+ edits = params[:edits]
+
+ resolved = begin
+ Sandbox.resolve_path(path, worktree_path:)
+ rescue SandboxError => e
+ next Dispatch::Tools::Result.failure(error: e.message)
+ end
+
+ unless File.exist?(resolved)
+ next Dispatch::Tools::Result.failure(error: "File not found: #{path}")
+ end
+
+ content = File.read(resolved)
+ edits_applied = 0
+ error_result = nil
+
+ edits.each do |edit|
+ old_text = edit[:old_text] || edit["old_text"]
+ new_text = edit[:new_text] || edit["new_text"]
+
+ escaped = Regexp.new(Regexp.escape(old_text))
+ occurrences = content.scan(escaped).length
+
+ if occurrences.zero?
+ error_result = Dispatch::Tools::Result.failure(
+ error: "Edit #{edits_applied + 1}: old_text not found in #{path}"
+ )
+ break
+ end
+
+ if occurrences > 1
+ error_result = Dispatch::Tools::Result.failure(
+ error: "Edit #{edits_applied + 1}: ambiguous match — old_text found #{occurrences} times in #{path}. " \
+ "Provide more context to make the match unique."
+ )
+ break
+ end
+
+ content = content.sub(old_text, new_text)
+ edits_applied += 1
+ end
+
+ next error_result if error_result
+
+ File.write(resolved, content)
+ Dispatch::Tools::Result.success(output: "Applied #{edits_applied} edit(s) to #{path}")
+ end
+ end
+ end
+end