Class: Ace::LLM::Molecules::FileIoHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/ace/llm/molecules/file_io_handler.rb

Overview

FileIoHandler provides file I/O utilities for LLM query commands This is a molecule - it handles specific file operations with validation

Constant Summary collapse

FORMAT_EXTENSIONS =

File extensions that indicate different output formats

{
  ".json" => "json",
  ".md" => "markdown",
  ".markdown" => "markdown",
  ".txt" => "text",
  ".text" => "text"
}.freeze
MAX_FILE_SIZE =

Maximum file size to read (10MB)

10 * 1024 * 1024

Instance Method Summary collapse

Constructor Details

#initialize(**options) ⇒ FileIoHandler

Initialize file I/O handler

Parameters:

  • options (Hash)

    Configuration options

Options Hash (**options):

  • :max_file_size (Integer)

    Maximum file size to read



27
28
29
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 27

def initialize(**options)
  @max_file_size = options.fetch(:max_file_size, MAX_FILE_SIZE)
end

Instance Method Details

#file_path?(input) ⇒ Boolean

Detect if input is a file path or inline content

Parameters:

  • input (String)

    Input string to analyze

Returns:

  • (Boolean)

    True if input appears to be a file path



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 34

def file_path?(input)
  return false if input.nil? || input.strip.empty?

  # File paths must be single line strings
  input_str = input.strip
  return false if input_str.include?("\n") || input_str.include?("\r")

  # Only consider it a file path if the file actually exists
  begin
    path = Pathname.new(input_str)
    File.exist?(path.to_s)
  rescue ArgumentError, SystemCallError
    # Invalid path characters or other path-related errors
    false
  end
end

#infer_format(file_path) ⇒ String

Infer output format from file extension

Parameters:

  • file_path (String)

    File path

Returns:

  • (String)

    Format name (json, markdown, or text)



128
129
130
131
132
133
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 128

def infer_format(file_path)
  return "text" if file_path.nil? || file_path.empty?

  ext = File.extname(file_path).downcase
  FORMAT_EXTENSIONS.fetch(ext, "text")
end

#read_content(input, auto_detect: true) ⇒ String

Read content from file or return inline content

Parameters:

  • input (String)

    File path or inline content

  • auto_detect (Boolean) (defaults to: true)

    Whether to auto-detect file vs inline content

Returns:

  • (String)

    Content text

Raises:

  • (Error)

    If file cannot be read or is too large



56
57
58
59
60
61
62
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 56

def read_content(input, auto_detect: true)
  if auto_detect && file_path?(input)
    read_file_content(input.strip)
  else
    validate_inline_content(input)
  end
end

#read_file_content(file_path) ⇒ String

Read content from a file with size validation

Parameters:

  • file_path (String)

    Path to file to read

Returns:

  • (String)

    File content

Raises:

  • (Error)

    If file cannot be read or is too large



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 68

def read_file_content(file_path)
  path = Pathname.new(file_path).expand_path

  # Check file size
  file_size = File.size(path)
  if file_size > @max_file_size
    raise Ace::LLM::Error, "File too large: #{file_size} bytes (max: #{@max_file_size} bytes)"
  end

  # Read file content
  File.read(path)
rescue Errno::ENOENT
  raise Ace::LLM::Error, "File not found: #{file_path}"
rescue Errno::EACCES
  raise Ace::LLM::Error, "Permission denied reading file: #{file_path}"
rescue SystemCallError => e
  raise Ace::LLM::Error, "Error reading file: #{e.message}"
end

#safe_path?(path) ⇒ Boolean

Check if a path is safe to write to

Parameters:

  • path (String)

    Path to check

Returns:

  • (Boolean)

    True if path appears safe



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 138

def safe_path?(path)
  return false if path.nil? || path.empty?

  # Reject paths with null bytes
  return false if path.include?("\0")

  # Reject paths trying to traverse up directories
  return false if path.include?("..")

  begin
    expanded = Pathname.new(path).expand_path
    # Must be absolute after expansion
    expanded.absolute?
  rescue
    false
  end
end

#validate_inline_content(content) ⇒ String

Validate inline content

Parameters:

  • content (String)

    Content to validate

Returns:

  • (String)

    The content (unchanged if valid)

Raises:

  • (Error)

    If content is invalid



91
92
93
94
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 91

def validate_inline_content(content)
  raise Ace::LLM::Error, "Content cannot be nil or empty" if content.nil? || content.strip.empty?
  content
end

#write_content(content, file_path, format: nil, force: false) ⇒ String

Write content to file with format handling

Parameters:

  • content (String)

    Content to write

  • file_path (String)

    Output file path

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

    Format override (json, markdown, text)

  • force (Boolean) (defaults to: false)

    Whether to force overwrite without confirmation

Returns:

  • (String)

    Inferred or specified format

Raises:

  • (Error)

    If file cannot be written



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/ace/llm/molecules/file_io_handler.rb', line 103

def write_content(content, file_path, format: nil, force: false)
  path = Pathname.new(file_path).expand_path

  # Check if file exists and handle overwrite
  if !force && File.exist?(path)
    raise Ace::LLM::Error, "File already exists: #{file_path}. Use --force to overwrite."
  end

  # Ensure parent directory exists
  FileUtils.mkdir_p(path.dirname)

  # Write content
  File.write(path, content)

  # Return format (inferred from extension or specified)
  format || infer_format(file_path)
rescue Errno::EACCES
  raise Ace::LLM::Error, "Permission denied writing to: #{file_path}"
rescue SystemCallError => e
  raise Ace::LLM::Error, "Error writing file: #{e.message}"
end