Class: Brute::Tools::FSRead

Inherits:
Brute::Tool show all
Defined in:
lib/brute/tools/fs_read.rb

Overview

Existing features (ref: opencode read tool):

  1. Default line limit — cap reads at 2000 lines when no start_line/end_line given, instead of reading the entire file.
  2. Byte cap — stop reading when cumulative output exceeds 50 KB (MAX_BYTES). Whichever limit (lines or bytes) is hit first wins.
  3. Per-line truncation — truncate individual lines longer than 2000 chars with a suffix like "... (line truncated to 2000 chars)".
  4. Pagination hint — when output is truncated, append a hint: "(Showing lines 1-N of M. Use start_line=N+1 to continue.)" When reading completes, append "(End of file - total N lines)".
  5. Binary file detection — read first 4 KB sample, check for null bytes and known binary extensions (.zip, .exe, .so, .pyc, etc.). Reject with "Cannot read binary file: (path)".
  6. Directory listing — when file_path points to a directory, list entries (paginated, respecting limit) instead of raising an error.
  7. File-not-found suggestions — on miss, scan the parent directory for similar names and suggest "Did you mean...?" candidates.
  8. Return a plain string instead of a Hash — avoids the .to_s repr bloat when ToolPipeline coerces the result for the LLM message.

Constant Summary collapse

BINARY_EXTENSIONS =
%w[.zip .exe .so .pyc .pyo .dll .dylib .bin .o .a .tar .gz .bz2 .xz .7z .rar .jar .war .class .png .jpg .jpeg .gif .bmp .ico .pdf .woff .woff2 .ttf .eot .mp3 .mp4 .avi .mov .flv .wmv .db .sqlite .sqlite3].freeze
DEFAULT_LINE_CAP =
2000
MAX_BYTES =
Brute::Truncation::MAX_BYTES
MAX_LINE_LENGTH =
Brute::Truncation::MAX_LINE_LENGTH

Instance Method Summary collapse

Methods inherited from Brute::Tool

#call, description, #description, param, param_definitions, params, #params, #params_schema

Instance Method Details

#execute(file_path:, start_line: nil, end_line: nil) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
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
111
112
113
114
115
116
# File 'lib/brute/tools/fs_read.rb', line 46

def execute(file_path:, start_line: nil, end_line: nil)
  path = File.expand_path(file_path)

  # Directory listing
  if File.directory?(path)
    list_directory(path)
  else
    # File-not-found suggestions
    unless File.exist?(path)
      suggestions = find_similar(path)
      msg = "File not found: #{path}"
      if suggestions.any?
        msg += ". Did you mean: #{suggestions.join(', ')}?"
      end
      raise msg
    end

    unless File.file?(path)
      raise "Not a file: #{path}"
    end

    # Binary file detection
    ext = File.extname(path).downcase
    if BINARY_EXTENSIONS.include?(ext)
      raise "Cannot read binary file: #{path}"
    end
    sample = File.binread(path, 4096) || ""
    if sample.include?("\x00")
      raise "Cannot read binary file: #{path}"
    end

    lines = File.readlines(path)
    total = lines.size
    if start_line
      first = [start_line - 1, 0].max
    else
      first = 0
    end

    # Apply default line cap when no explicit range given
    if end_line
      default_last = [end_line - 1, total - 1].min
    else
      default_last = [first + DEFAULT_LINE_CAP - 1, total - 1].min
    end
    last = default_last
    selected = lines[first..last] || []

    # Per-line truncation + byte cap
    numbered = []
    bytes = 0
    selected.each_with_index do |line, i|
      truncated_line = Brute::Truncation.truncate_line(line, max: MAX_LINE_LENGTH)
      numbered_line = "#{first + i + 1}\t#{truncated_line}"
      if bytes + numbered_line.bytesize > MAX_BYTES
        break
      end
      numbered << numbered_line
      bytes += numbered_line.bytesize
    end

    actual_last = first + numbered.size - 1
    content = numbered.join
    truncated = (actual_last < total - 1) && end_line.nil?
    if truncated
      content + "\n(Showing lines #{first + 1}-#{actual_last + 1} of #{total}. Use start_line=#{actual_last + 2} to continue.)"
    else
      content
    end
  end
end

#nameObject



39
# File 'lib/brute/tools/fs_read.rb', line 39

def name; "read"; end