summaryrefslogtreecommitdiffhomepage
path: root/lib/dispatch/tool/files/search_files.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/search_files.rb
parent25488d32336e05b69a41391cc7b5153478d3cc8a (diff)
downloaddispatch-tool-files-57c56daf5906442dacc15951c9b3405f89309839.tar.gz
dispatch-tool-files-57c56daf5906442dacc15951c9b3405f89309839.zip
impdev
Diffstat (limited to 'lib/dispatch/tool/files/search_files.rb')
-rw-r--r--lib/dispatch/tool/files/search_files.rb89
1 files changed, 89 insertions, 0 deletions
diff --git a/lib/dispatch/tool/files/search_files.rb b/lib/dispatch/tool/files/search_files.rb
new file mode 100644
index 0000000..b0ccc9e
--- /dev/null
+++ b/lib/dispatch/tool/files/search_files.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+module Dispatch
+ module Tool
+ module Files
+ SEARCH_FILES = Dispatch::Tools::Definition.new(
+ name: "search_files",
+ description: "Search for text in files. Returns matching lines with file paths and line numbers.",
+ parameters: {
+ type: "object",
+ properties: {
+ query: {
+ type: "string",
+ description: "The text or regex pattern to search for."
+ },
+ path: {
+ type: "string",
+ description: "Directory to search within, relative to the worktree root. Defaults to root."
+ },
+ pattern: {
+ type: "string",
+ description: "Glob pattern to filter which files to search (e.g. '**/*.rb')."
+ },
+ is_regex: {
+ type: "boolean",
+ description: "Whether the query is a regular expression. Defaults to false."
+ }
+ },
+ required: %w[query],
+ additionalProperties: false
+ }
+ ) do |params, context|
+ worktree_path = context[:worktree_path]
+ query = params[:query]
+ path = params.fetch(:path, ".")
+ file_pattern = params[:pattern]
+ is_regex = params.fetch(:is_regex, false)
+
+ resolved = begin
+ Sandbox.resolve_path(path, worktree_path:)
+ rescue SandboxError => e
+ next Dispatch::Tools::Result.failure(error: e.message)
+ end
+
+ regex = if is_regex
+ begin
+ Regexp.new(query)
+ rescue RegexpError => e
+ next Dispatch::Tools::Result.failure(error: "Invalid regex: #{e.message}")
+ end
+ else
+ Regexp.new(Regexp.escape(query))
+ end
+
+ # Build file list
+ glob = if file_pattern
+ File.join(resolved, file_pattern)
+ else
+ File.join(resolved, "**", "*")
+ end
+
+ files = Dir.glob(glob).select { |f| File.file?(f) }.sort
+
+ worktree_real = File.realpath(worktree_path)
+ max_results = 100
+ matches = []
+
+ files.each do |file_path|
+ # Skip binary files
+ sample = File.binread(file_path, 8192)
+ next if sample&.include?("\x00")
+
+ relative = file_path.delete_prefix("#{worktree_real}/")
+
+ File.readlines(file_path).each_with_index do |line, idx|
+ if regex.match?(line)
+ matches << "#{relative}:#{idx + 1}: #{line.chomp}"
+ break if matches.length >= max_results
+ end
+ end
+
+ break if matches.length >= max_results
+ end
+
+ Dispatch::Tools::Result.success(output: matches.join("\n"))
+ end
+ end
+ end
+end