rcrewai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +108 -0
  3. data/LICENSE +21 -0
  4. data/README.md +328 -0
  5. data/Rakefile +130 -0
  6. data/bin/rcrewai +7 -0
  7. data/docs/_config.yml +59 -0
  8. data/docs/_layouts/api.html +16 -0
  9. data/docs/_layouts/default.html +78 -0
  10. data/docs/_layouts/example.html +24 -0
  11. data/docs/_layouts/tutorial.html +33 -0
  12. data/docs/api/configuration.md +327 -0
  13. data/docs/api/crew.md +345 -0
  14. data/docs/api/index.md +41 -0
  15. data/docs/api/tools.md +412 -0
  16. data/docs/assets/css/style.css +416 -0
  17. data/docs/examples/human-in-the-loop.md +382 -0
  18. data/docs/examples/index.md +78 -0
  19. data/docs/examples/production-ready-crew.md +485 -0
  20. data/docs/examples/simple-research-crew.md +297 -0
  21. data/docs/index.md +353 -0
  22. data/docs/tutorials/getting-started.md +341 -0
  23. data/examples/async_execution_example.rb +294 -0
  24. data/examples/hierarchical_crew_example.rb +193 -0
  25. data/examples/human_in_the_loop_example.rb +233 -0
  26. data/lib/rcrewai/agent.rb +636 -0
  27. data/lib/rcrewai/async_executor.rb +248 -0
  28. data/lib/rcrewai/cli.rb +39 -0
  29. data/lib/rcrewai/configuration.rb +100 -0
  30. data/lib/rcrewai/crew.rb +292 -0
  31. data/lib/rcrewai/human_input.rb +520 -0
  32. data/lib/rcrewai/llm_client.rb +41 -0
  33. data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
  34. data/lib/rcrewai/llm_clients/azure.rb +158 -0
  35. data/lib/rcrewai/llm_clients/base.rb +82 -0
  36. data/lib/rcrewai/llm_clients/google.rb +158 -0
  37. data/lib/rcrewai/llm_clients/ollama.rb +199 -0
  38. data/lib/rcrewai/llm_clients/openai.rb +124 -0
  39. data/lib/rcrewai/memory.rb +194 -0
  40. data/lib/rcrewai/process.rb +421 -0
  41. data/lib/rcrewai/task.rb +376 -0
  42. data/lib/rcrewai/tools/base.rb +82 -0
  43. data/lib/rcrewai/tools/code_executor.rb +333 -0
  44. data/lib/rcrewai/tools/email_sender.rb +210 -0
  45. data/lib/rcrewai/tools/file_reader.rb +111 -0
  46. data/lib/rcrewai/tools/file_writer.rb +115 -0
  47. data/lib/rcrewai/tools/pdf_processor.rb +342 -0
  48. data/lib/rcrewai/tools/sql_database.rb +226 -0
  49. data/lib/rcrewai/tools/web_search.rb +131 -0
  50. data/lib/rcrewai/version.rb +5 -0
  51. data/lib/rcrewai.rb +36 -0
  52. data/rcrewai.gemspec +54 -0
  53. metadata +365 -0
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'open3'
5
+ require 'tempfile'
6
+ require 'fileutils'
7
+
8
+ module RCrewAI
9
+ module Tools
10
+ class CodeExecutor < Base
11
+ def initialize(**options)
12
+ super()
13
+ @name = 'codeexecutor'
14
+ @description = 'Execute code in various programming languages (Python, Ruby, JavaScript, etc.)'
15
+ @timeout = options.fetch(:timeout, 30)
16
+ @max_output_size = options.fetch(:max_output_size, 100_000) # 100KB
17
+ @allowed_languages = options.fetch(:allowed_languages, %w[python ruby javascript bash])
18
+ @working_directory = options[:working_directory] || Dir.mktmpdir('rcrewai_code_')
19
+ @enable_file_operations = options.fetch(:enable_file_operations, false)
20
+ setup_security_restrictions
21
+ end
22
+
23
+ def execute(**params)
24
+ validate_params!(params, required: [:code, :language], optional: [:args, :stdin])
25
+
26
+ language = params[:language].to_s.downcase
27
+ code = params[:code]
28
+ args = params[:args] || []
29
+ stdin_input = params[:stdin]
30
+
31
+ begin
32
+ validate_execution_params!(language, code)
33
+ result = execute_code(language, code, args, stdin_input)
34
+ format_execution_result(language, code, result)
35
+ rescue => e
36
+ "Code execution failed: #{e.message}"
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def setup_security_restrictions
43
+ # Create secure working directory
44
+ FileUtils.mkdir_p(@working_directory)
45
+ File.chmod(0o755, @working_directory)
46
+
47
+ # Set environment variables for security
48
+ @secure_env = {
49
+ 'PATH' => '/usr/local/bin:/usr/bin:/bin',
50
+ 'HOME' => @working_directory,
51
+ 'TMPDIR' => @working_directory,
52
+ 'TEMP' => @working_directory,
53
+ 'TMP' => @working_directory
54
+ }
55
+ end
56
+
57
+ def validate_execution_params!(language, code)
58
+ unless @allowed_languages.include?(language)
59
+ raise ToolError, "Language not allowed: #{language}. Allowed: #{@allowed_languages.join(', ')}"
60
+ end
61
+
62
+ if code.to_s.strip.empty?
63
+ raise ToolError, "Code cannot be empty"
64
+ end
65
+
66
+ if code.length > 50_000 # 50KB max
67
+ raise ToolError, "Code too long: #{code.length} characters (max: 50,000)"
68
+ end
69
+
70
+ validate_code_safety!(language, code)
71
+ end
72
+
73
+ def validate_code_safety!(language, code)
74
+ code_lower = code.downcase
75
+
76
+ # Common dangerous patterns across languages
77
+ dangerous_patterns = [
78
+ # System operations
79
+ 'system(', 'exec(', 'eval(', 'subprocess', 'os.system',
80
+ 'shell_exec', 'passthru', '`', 'require "open3"',
81
+
82
+ # File operations (if not explicitly enabled)
83
+ 'file.open', 'open(', 'with open', 'fopen', 'file_get_contents',
84
+ 'file_put_contents', 'unlink', 'delete', 'remove',
85
+
86
+ # Network operations
87
+ 'socket', 'http', 'urllib', 'requests', 'net::',
88
+ 'tcpsocket', 'udpsocket', 'curl', 'wget',
89
+
90
+ # Process operations
91
+ 'fork', 'spawn', 'thread', 'process',
92
+
93
+ # Dangerous Python imports
94
+ 'import os', 'import sys', 'import subprocess', 'import socket',
95
+ 'from os', 'from sys', 'from subprocess',
96
+
97
+ # Dangerous Ruby requires
98
+ 'require "fileutils"', 'require "open3"', 'require "net/',
99
+
100
+ # Shell commands
101
+ 'rm -', 'sudo', 'chmod', 'chown', 'dd if=', 'mkfs',
102
+ 'iptables', 'systemctl', 'service '
103
+ ]
104
+
105
+ unless @enable_file_operations
106
+ dangerous_patterns += [
107
+ 'file.', 'open(', 'with open', 'fopen', 'file_get_contents',
108
+ 'file_put_contents', 'fileutils', 'fs.', 'path.', 'dir.'
109
+ ]
110
+ end
111
+
112
+ dangerous_patterns.each do |pattern|
113
+ if code_lower.include?(pattern)
114
+ raise ToolError, "Potentially dangerous code detected: #{pattern}"
115
+ end
116
+ end
117
+
118
+ # Language-specific checks
119
+ case language
120
+ when 'python'
121
+ validate_python_safety!(code_lower)
122
+ when 'ruby'
123
+ validate_ruby_safety!(code_lower)
124
+ when 'javascript', 'node', 'js'
125
+ validate_javascript_safety!(code_lower)
126
+ when 'bash', 'sh'
127
+ validate_bash_safety!(code_lower)
128
+ end
129
+ end
130
+
131
+ def validate_python_safety!(code)
132
+ python_dangerous = [
133
+ '__import__', 'getattr', 'setattr', 'delattr', 'hasattr',
134
+ 'globals(', 'locals(', 'vars(', 'dir(',
135
+ 'compile(', 'exec(', 'eval(',
136
+ 'input(', 'raw_input('
137
+ ]
138
+
139
+ python_dangerous.each do |pattern|
140
+ if code.include?(pattern)
141
+ raise ToolError, "Dangerous Python construct: #{pattern}"
142
+ end
143
+ end
144
+ end
145
+
146
+ def validate_ruby_safety!(code)
147
+ ruby_dangerous = [
148
+ 'instance_eval', 'class_eval', 'module_eval',
149
+ 'define_method', 'send(', 'method(',
150
+ 'const_get', 'const_set', 'remove_const',
151
+ 'gets', 'readline', 'readlines'
152
+ ]
153
+
154
+ ruby_dangerous.each do |pattern|
155
+ if code.include?(pattern)
156
+ raise ToolError, "Dangerous Ruby construct: #{pattern}"
157
+ end
158
+ end
159
+ end
160
+
161
+ def validate_javascript_safety!(code)
162
+ js_dangerous = [
163
+ 'eval(', 'function(', '=>', 'require(',
164
+ 'process.', 'global.', 'this.',
165
+ 'document.', 'window.', 'location.',
166
+ 'settimeout', 'setinterval'
167
+ ]
168
+
169
+ js_dangerous.each do |pattern|
170
+ if code.include?(pattern)
171
+ raise ToolError, "Dangerous JavaScript construct: #{pattern}"
172
+ end
173
+ end
174
+ end
175
+
176
+ def validate_bash_safety!(code)
177
+ bash_dangerous = [
178
+ 'rm ', 'sudo', 'su ', 'chmod', 'chown',
179
+ '>', '>>', '|', '&', ';',
180
+ '$(', '`', 'eval ', 'exec ',
181
+ '/etc/', '/proc/', '/sys/', '/dev/'
182
+ ]
183
+
184
+ bash_dangerous.each do |pattern|
185
+ if code.include?(pattern)
186
+ raise ToolError, "Dangerous bash construct: #{pattern}"
187
+ end
188
+ end
189
+ end
190
+
191
+ def execute_code(language, code, args, stdin_input)
192
+ case language
193
+ when 'python', 'python3'
194
+ execute_python(code, args, stdin_input)
195
+ when 'ruby'
196
+ execute_ruby(code, args, stdin_input)
197
+ when 'javascript', 'node', 'js'
198
+ execute_javascript(code, args, stdin_input)
199
+ when 'bash', 'sh'
200
+ execute_bash(code, args, stdin_input)
201
+ else
202
+ raise ToolError, "Execution not implemented for language: #{language}"
203
+ end
204
+ end
205
+
206
+ def execute_python(code, args, stdin_input)
207
+ with_temp_file(code, '.py') do |file_path|
208
+ command = ['python3', file_path] + args.map(&:to_s)
209
+ execute_command(command, stdin_input)
210
+ end
211
+ end
212
+
213
+ def execute_ruby(code, args, stdin_input)
214
+ with_temp_file(code, '.rb') do |file_path|
215
+ command = ['ruby', file_path] + args.map(&:to_s)
216
+ execute_command(command, stdin_input)
217
+ end
218
+ end
219
+
220
+ def execute_javascript(code, args, stdin_input)
221
+ with_temp_file(code, '.js') do |file_path|
222
+ command = ['node', file_path] + args.map(&:to_s)
223
+ execute_command(command, stdin_input)
224
+ end
225
+ end
226
+
227
+ def execute_bash(code, args, stdin_input)
228
+ with_temp_file(code, '.sh') do |file_path|
229
+ File.chmod(0o755, file_path)
230
+ command = ['bash', file_path] + args.map(&:to_s)
231
+ execute_command(command, stdin_input)
232
+ end
233
+ end
234
+
235
+ def with_temp_file(content, extension)
236
+ temp_file = Tempfile.new(['rcrewai_code', extension], @working_directory)
237
+ begin
238
+ temp_file.write(content)
239
+ temp_file.flush
240
+ temp_file.close
241
+ yield temp_file.path
242
+ ensure
243
+ temp_file.unlink if temp_file
244
+ end
245
+ end
246
+
247
+ def execute_command(command, stdin_input)
248
+ stdout_str = ""
249
+ stderr_str = ""
250
+ exit_status = nil
251
+
252
+ # Execute with timeout and security restrictions
253
+ Open3.popen3(@secure_env, *command, chdir: @working_directory) do |stdin, stdout, stderr, wait_thread|
254
+ # Write stdin if provided
255
+ if stdin_input
256
+ stdin.write(stdin_input)
257
+ stdin.close
258
+ end
259
+
260
+ # Set up timeout
261
+ timeout_thread = Thread.new do
262
+ sleep @timeout
263
+ Process.kill('KILL', wait_thread.pid) rescue nil
264
+ end
265
+
266
+ begin
267
+ # Read output
268
+ stdout_str = read_with_limit(stdout, @max_output_size)
269
+ stderr_str = read_with_limit(stderr, @max_output_size)
270
+
271
+ exit_status = wait_thread.value.exitstatus
272
+ rescue => e
273
+ exit_status = -1
274
+ stderr_str += "\nExecution error: #{e.message}"
275
+ ensure
276
+ timeout_thread.kill
277
+ end
278
+ end
279
+
280
+ {
281
+ stdout: stdout_str,
282
+ stderr: stderr_str,
283
+ exit_status: exit_status,
284
+ success: exit_status == 0
285
+ }
286
+ end
287
+
288
+ def read_with_limit(io, limit)
289
+ output = ""
290
+ while output.bytesize < limit && !io.eof?
291
+ chunk = io.read(1024)
292
+ break unless chunk
293
+
294
+ if output.bytesize + chunk.bytesize > limit
295
+ remaining = limit - output.bytesize
296
+ output += chunk[0, remaining]
297
+ output += "\n[OUTPUT TRUNCATED - LIMIT EXCEEDED]"
298
+ break
299
+ else
300
+ output += chunk
301
+ end
302
+ end
303
+ output
304
+ end
305
+
306
+ def format_execution_result(language, code, result)
307
+ output = []
308
+ output << "Code Execution Result (#{language.upcase})"
309
+ output << "=" * 40
310
+ output << "Exit Status: #{result[:exit_status]} (#{result[:success] ? 'SUCCESS' : 'FAILURE'})"
311
+ output << ""
312
+
313
+ if result[:stdout] && !result[:stdout].empty?
314
+ output << "STDOUT:"
315
+ output << result[:stdout]
316
+ output << ""
317
+ end
318
+
319
+ if result[:stderr] && !result[:stderr].empty?
320
+ output << "STDERR:"
321
+ output << result[:stderr]
322
+ output << ""
323
+ end
324
+
325
+ if result[:stdout].empty? && result[:stderr].empty?
326
+ output << "No output produced."
327
+ end
328
+
329
+ output.join("\n")
330
+ end
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'net/smtp'
5
+ require 'mail'
6
+
7
+ module RCrewAI
8
+ module Tools
9
+ class EmailSender < Base
10
+ def initialize(**options)
11
+ super()
12
+ @name = 'emailsender'
13
+ @description = 'Send emails via SMTP'
14
+ @smtp_server = options[:smtp_server] || 'localhost'
15
+ @smtp_port = options[:smtp_port] || 587
16
+ @username = options[:username]
17
+ @password = options[:password]
18
+ @from_address = options[:from_address] || @username
19
+ @use_tls = options.fetch(:use_tls, true)
20
+ @max_recipients = options.fetch(:max_recipients, 10)
21
+ setup_mail_configuration
22
+ end
23
+
24
+ def execute(**params)
25
+ validate_params!(
26
+ params,
27
+ required: [:to, :subject, :body],
28
+ optional: [:cc, :bcc, :reply_to, :attachments]
29
+ )
30
+
31
+ to_addresses = normalize_email_addresses(params[:to])
32
+ cc_addresses = normalize_email_addresses(params[:cc]) if params[:cc]
33
+ bcc_addresses = normalize_email_addresses(params[:bcc]) if params[:bcc]
34
+
35
+ begin
36
+ validate_email_params!(to_addresses, cc_addresses, bcc_addresses, params)
37
+ result = send_email(to_addresses, cc_addresses, bcc_addresses, params)
38
+ format_email_result(result, to_addresses)
39
+ rescue => e
40
+ "Email sending failed: #{e.message}"
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def setup_mail_configuration
47
+ Mail.defaults do
48
+ delivery_method :smtp, {
49
+ address: @smtp_server,
50
+ port: @smtp_port,
51
+ user_name: @username,
52
+ password: @password,
53
+ authentication: 'login',
54
+ enable_starttls_auto: @use_tls
55
+ }
56
+ end
57
+ end
58
+
59
+ def normalize_email_addresses(addresses)
60
+ case addresses
61
+ when String
62
+ addresses.split(/[,;]/).map(&:strip)
63
+ when Array
64
+ addresses.map(&:to_s).map(&:strip)
65
+ else
66
+ [addresses.to_s.strip]
67
+ end
68
+ end
69
+
70
+ def validate_email_params!(to_addresses, cc_addresses, bcc_addresses, params)
71
+ # Validate email addresses
72
+ all_addresses = to_addresses + (cc_addresses || []) + (bcc_addresses || [])
73
+
74
+ all_addresses.each do |address|
75
+ unless valid_email?(address)
76
+ raise ToolError, "Invalid email address: #{address}"
77
+ end
78
+ end
79
+
80
+ # Check recipient limits
81
+ if all_addresses.length > @max_recipients
82
+ raise ToolError, "Too many recipients: #{all_addresses.length} (max: #{@max_recipients})"
83
+ end
84
+
85
+ # Validate subject and body
86
+ if params[:subject].to_s.strip.empty?
87
+ raise ToolError, "Email subject cannot be empty"
88
+ end
89
+
90
+ if params[:body].to_s.strip.empty?
91
+ raise ToolError, "Email body cannot be empty"
92
+ end
93
+
94
+ # Check for spam-like content
95
+ validate_content_safety!(params[:subject], params[:body])
96
+ end
97
+
98
+ def valid_email?(address)
99
+ address.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
100
+ end
101
+
102
+ def validate_content_safety!(subject, body)
103
+ # Basic spam detection
104
+ spam_indicators = [
105
+ 'viagra', 'casino', 'lottery', 'winner', 'congratulations',
106
+ 'million dollars', 'click here', 'urgent', 'act now'
107
+ ]
108
+
109
+ content = "#{subject} #{body}".downcase
110
+
111
+ spam_indicators.each do |indicator|
112
+ if content.include?(indicator)
113
+ raise ToolError, "Potentially spam content detected: #{indicator}"
114
+ end
115
+ end
116
+
117
+ # Check for excessive caps
118
+ caps_ratio = content.gsub(/[^A-Z]/, '').length.to_f / content.gsub(/[^A-Za-z]/, '').length
119
+ if caps_ratio > 0.5 && content.length > 50
120
+ raise ToolError, "Excessive capitalization detected (possible spam)"
121
+ end
122
+ end
123
+
124
+ def send_email(to_addresses, cc_addresses, bcc_addresses, params)
125
+ mail = Mail.new do
126
+ from @from_address
127
+ to to_addresses
128
+ cc cc_addresses if cc_addresses&.any?
129
+ bcc bcc_addresses if bcc_addresses&.any?
130
+ subject params[:subject]
131
+ body params[:body]
132
+
133
+ if params[:reply_to]
134
+ reply_to params[:reply_to]
135
+ end
136
+ end
137
+
138
+ # Add attachments if provided
139
+ if params[:attachments]
140
+ add_attachments(mail, params[:attachments])
141
+ end
142
+
143
+ # Send the email
144
+ mail.deliver!
145
+
146
+ {
147
+ message_id: mail.message_id,
148
+ to: to_addresses,
149
+ cc: cc_addresses,
150
+ bcc: bcc_addresses,
151
+ subject: params[:subject],
152
+ sent_at: Time.now,
153
+ size: mail.to_s.bytesize
154
+ }
155
+ end
156
+
157
+ def add_attachments(mail, attachments)
158
+ attachment_list = case attachments
159
+ when String
160
+ [attachments]
161
+ when Array
162
+ attachments
163
+ else
164
+ [attachments.to_s]
165
+ end
166
+
167
+ attachment_list.each do |attachment_path|
168
+ if File.exist?(attachment_path)
169
+ # Security check - only allow certain file types
170
+ allowed_extensions = %w[.pdf .txt .doc .docx .xls .xlsx .jpg .png .gif .zip]
171
+ extension = File.extname(attachment_path).downcase
172
+
173
+ unless allowed_extensions.include?(extension)
174
+ raise ToolError, "Attachment file type not allowed: #{extension}"
175
+ end
176
+
177
+ # Size check (max 10MB per attachment)
178
+ if File.size(attachment_path) > 10_000_000
179
+ raise ToolError, "Attachment too large: #{File.basename(attachment_path)} (max 10MB)"
180
+ end
181
+
182
+ mail.add_file(attachment_path)
183
+ else
184
+ raise ToolError, "Attachment file not found: #{attachment_path}"
185
+ end
186
+ end
187
+ end
188
+
189
+ def format_email_result(result, to_addresses)
190
+ output = []
191
+ output << "Email sent successfully!"
192
+ output << "Message ID: #{result[:message_id]}"
193
+ output << "Recipients: #{to_addresses.join(', ')}"
194
+ output << "Subject: #{result[:subject]}"
195
+ output << "Sent at: #{result[:sent_at].strftime('%Y-%m-%d %H:%M:%S')}"
196
+ output << "Message size: #{result[:size]} bytes"
197
+
198
+ if result[:cc]&.any?
199
+ output << "CC: #{result[:cc].join(', ')}"
200
+ end
201
+
202
+ if result[:bcc]&.any?
203
+ output << "BCC: #{result[:bcc].length} recipients"
204
+ end
205
+
206
+ output.join("\n")
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'pathname'
5
+
6
+ module RCrewAI
7
+ module Tools
8
+ class FileReader < Base
9
+ def initialize(**options)
10
+ super()
11
+ @name = 'filereader'
12
+ @description = 'Read contents from files'
13
+ @max_file_size = options.fetch(:max_file_size, 10_000_000) # 10MB
14
+ @allowed_extensions = options.fetch(:allowed_extensions, %w[.txt .md .json .yaml .yml .csv .log])
15
+ end
16
+
17
+ def execute(**params)
18
+ validate_params!(params, required: [:file_path], optional: [:encoding, :lines])
19
+
20
+ file_path = params[:file_path]
21
+ encoding = params[:encoding] || 'utf-8'
22
+ lines = params[:lines] # Optional: read only N lines
23
+
24
+ begin
25
+ read_file(file_path, encoding, lines)
26
+ rescue => e
27
+ "File read failed: #{e.message}"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def read_file(file_path, encoding, lines = nil)
34
+ path = Pathname.new(file_path)
35
+
36
+ # Security checks
37
+ validate_file_path!(path)
38
+ validate_file_size!(path)
39
+ validate_file_extension!(path)
40
+
41
+ content = if lines
42
+ read_lines(path, encoding, lines)
43
+ else
44
+ read_full_file(path, encoding)
45
+ end
46
+
47
+ format_file_content(path, content, lines)
48
+ end
49
+
50
+ def validate_file_path!(path)
51
+ raise ToolError, "File does not exist: #{path}" unless path.exist?
52
+ raise ToolError, "Path is not a file: #{path}" unless path.file?
53
+ raise ToolError, "File is not readable: #{path}" unless path.readable?
54
+
55
+ # Prevent directory traversal
56
+ resolved_path = path.realpath.to_s
57
+ working_dir = Dir.pwd
58
+ unless resolved_path.start_with?(working_dir)
59
+ raise ToolError, "Access denied: file outside working directory"
60
+ end
61
+ end
62
+
63
+ def validate_file_size!(path)
64
+ size = path.size
65
+ if size > @max_file_size
66
+ raise ToolError, "File too large: #{size} bytes (max: #{@max_file_size})"
67
+ end
68
+ end
69
+
70
+ def validate_file_extension!(path)
71
+ extension = path.extname.downcase
72
+ unless @allowed_extensions.include?(extension) || @allowed_extensions.include?('*')
73
+ raise ToolError, "File type not allowed: #{extension}"
74
+ end
75
+ end
76
+
77
+ def read_lines(path, encoding, line_count)
78
+ lines = []
79
+ File.open(path, 'r', encoding: encoding) do |file|
80
+ line_count.times do
81
+ line = file.gets
82
+ break unless line
83
+ lines << line.chomp
84
+ end
85
+ end
86
+ lines.join("\n")
87
+ end
88
+
89
+ def read_full_file(path, encoding)
90
+ File.read(path, encoding: encoding)
91
+ end
92
+
93
+ def format_file_content(path, content, lines = nil)
94
+ header = "File: #{path.basename}"
95
+ header += " (first #{lines} lines)" if lines
96
+ header += "\nSize: #{path.size} bytes"
97
+ header += "\nModified: #{path.mtime.strftime('%Y-%m-%d %H:%M:%S')}"
98
+ header += "\n" + "="*50 + "\n"
99
+
100
+ # Truncate very long content for display
101
+ display_content = if content.length > 5000
102
+ content[0..4997] + "..."
103
+ else
104
+ content
105
+ end
106
+
107
+ header + display_content
108
+ end
109
+ end
110
+ end
111
+ end