rcrewai 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +99 -0
- data/CHANGELOG.md +24 -0
- data/README.md +2 -2
- data/Rakefile +53 -53
- data/bin/rcrewai +3 -3
- data/docs/mcp.md +109 -0
- data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
- data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
- data/docs/upgrading-to-0.3.md +163 -0
- data/examples/async_execution_example.rb +82 -81
- data/examples/hierarchical_crew_example.rb +68 -72
- data/examples/human_in_the_loop_example.rb +73 -74
- data/examples/mcp_example.rb +48 -0
- data/examples/native_tools_example.rb +64 -0
- data/examples/streaming_example.rb +56 -0
- data/lib/rcrewai/agent.rb +148 -287
- data/lib/rcrewai/async_executor.rb +43 -43
- data/lib/rcrewai/cli.rb +11 -11
- data/lib/rcrewai/configuration.rb +14 -9
- data/lib/rcrewai/crew.rb +56 -39
- data/lib/rcrewai/events.rb +30 -0
- data/lib/rcrewai/human_input.rb +104 -114
- data/lib/rcrewai/legacy_react_runner.rb +172 -0
- data/lib/rcrewai/llm_client.rb +1 -1
- data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
- data/lib/rcrewai/llm_clients/azure.rb +23 -128
- data/lib/rcrewai/llm_clients/base.rb +11 -7
- data/lib/rcrewai/llm_clients/google.rb +159 -95
- data/lib/rcrewai/llm_clients/ollama.rb +150 -106
- data/lib/rcrewai/llm_clients/openai.rb +140 -63
- data/lib/rcrewai/mcp/client.rb +101 -0
- data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
- data/lib/rcrewai/mcp/transport/http.rb +53 -0
- data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
- data/lib/rcrewai/mcp.rb +8 -0
- data/lib/rcrewai/memory.rb +45 -37
- data/lib/rcrewai/pricing.rb +34 -0
- data/lib/rcrewai/process.rb +86 -95
- data/lib/rcrewai/provider_schema.rb +38 -0
- data/lib/rcrewai/sse_parser.rb +55 -0
- data/lib/rcrewai/task.rb +56 -64
- data/lib/rcrewai/tool_runner.rb +132 -0
- data/lib/rcrewai/tool_schema.rb +97 -0
- data/lib/rcrewai/tools/base.rb +98 -37
- data/lib/rcrewai/tools/code_executor.rb +71 -74
- data/lib/rcrewai/tools/email_sender.rb +70 -78
- data/lib/rcrewai/tools/file_reader.rb +38 -30
- data/lib/rcrewai/tools/file_writer.rb +40 -38
- data/lib/rcrewai/tools/pdf_processor.rb +115 -130
- data/lib/rcrewai/tools/sql_database.rb +58 -55
- data/lib/rcrewai/tools/web_search.rb +26 -25
- data/lib/rcrewai/version.rb +2 -2
- data/lib/rcrewai.rb +18 -10
- data/rcrewai.gemspec +39 -39
- metadata +65 -47
data/lib/rcrewai/tools/base.rb
CHANGED
|
@@ -1,32 +1,72 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../tool_schema'
|
|
4
|
+
|
|
3
5
|
module RCrewAI
|
|
4
6
|
module Tools
|
|
5
7
|
class Base
|
|
6
|
-
|
|
8
|
+
extend RCrewAI::ToolSchema
|
|
7
9
|
|
|
8
10
|
def initialize
|
|
9
|
-
@name
|
|
10
|
-
|
|
11
|
+
# @name and @description are no longer set here.
|
|
12
|
+
# Instance #name and #description delegate to the class-level DSL
|
|
13
|
+
# (tool_name / description) via the fallback in the reader methods below.
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def name
|
|
17
|
+
@name || self.class.tool_name
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def description
|
|
21
|
+
@description || self.class.description
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def json_schema
|
|
25
|
+
self.class.json_schema
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute_with_validation(args_hash)
|
|
29
|
+
coerced = {}
|
|
30
|
+
schema_params = self.class.params
|
|
31
|
+
|
|
32
|
+
if schema_params.empty?
|
|
33
|
+
coerced = args_hash.transform_keys(&:to_sym)
|
|
34
|
+
return execute(**coerced)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
schema_params.each do |p|
|
|
38
|
+
key_str = p[:name].to_s
|
|
39
|
+
key_sym = p[:name].to_sym
|
|
40
|
+
if args_hash.key?(key_str)
|
|
41
|
+
raw = args_hash[key_str]
|
|
42
|
+
elsif args_hash.key?(key_sym)
|
|
43
|
+
raw = args_hash[key_sym]
|
|
44
|
+
else
|
|
45
|
+
raise ToolError, "missing required param: #{p[:name]}" if p[:required]
|
|
46
|
+
|
|
47
|
+
next
|
|
48
|
+
end
|
|
49
|
+
coerced[key_sym] = coerce(raw, p[:type], p[:name])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
execute(**coerced)
|
|
11
53
|
end
|
|
12
54
|
|
|
13
55
|
def execute(**params)
|
|
14
|
-
raise NotImplementedError,
|
|
56
|
+
raise NotImplementedError, 'Subclasses must implement #execute method'
|
|
15
57
|
end
|
|
16
58
|
|
|
17
59
|
def validate_params!(params, required: [], optional: [])
|
|
18
60
|
# Check required parameters
|
|
19
61
|
missing = required - params.keys
|
|
20
|
-
unless missing.empty?
|
|
21
|
-
raise ToolError, "Missing required parameters: #{missing.join(', ')}"
|
|
22
|
-
end
|
|
62
|
+
raise ToolError, "Missing required parameters: #{missing.join(', ')}" unless missing.empty?
|
|
23
63
|
|
|
24
64
|
# Check for unexpected parameters
|
|
25
65
|
allowed = required + optional
|
|
26
66
|
unexpected = params.keys - allowed
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
return if unexpected.empty?
|
|
68
|
+
|
|
69
|
+
raise ToolError, "Unexpected parameters: #{unexpected.join(', ')}"
|
|
30
70
|
end
|
|
31
71
|
|
|
32
72
|
def self.available_tools
|
|
@@ -43,40 +83,61 @@ module RCrewAI
|
|
|
43
83
|
|
|
44
84
|
def self.create_tool(tool_name, **options)
|
|
45
85
|
tool_class = case tool_name.to_s.downcase
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
when 'websearch', 'web_search'
|
|
87
|
+
WebSearch
|
|
88
|
+
when 'filereader', 'file_reader'
|
|
89
|
+
FileReader
|
|
90
|
+
when 'filewriter', 'file_writer'
|
|
91
|
+
FileWriter
|
|
92
|
+
when 'sqldatabase', 'sql_database', 'database'
|
|
93
|
+
SqlDatabase
|
|
94
|
+
when 'emailsender', 'email_sender', 'email'
|
|
95
|
+
EmailSender
|
|
96
|
+
when 'codeexecutor', 'code_executor', 'code'
|
|
97
|
+
CodeExecutor
|
|
98
|
+
when 'pdfprocessor', 'pdf_processor', 'pdf'
|
|
99
|
+
PdfProcessor
|
|
100
|
+
else
|
|
101
|
+
raise ToolError, "Unknown tool: #{tool_name}"
|
|
102
|
+
end
|
|
63
103
|
|
|
64
104
|
tool_class.new(**options)
|
|
65
105
|
end
|
|
66
106
|
|
|
67
107
|
def self.list_available_tools
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
108
|
+
available_tools.each_with_object({}) do |klass, h|
|
|
109
|
+
h[klass.tool_name] = klass.description
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def coerce(value, type, name)
|
|
116
|
+
case type
|
|
117
|
+
when :integer
|
|
118
|
+
return value if value.is_a?(Integer)
|
|
119
|
+
|
|
120
|
+
Integer(value.to_s)
|
|
121
|
+
when :number
|
|
122
|
+
return value if value.is_a?(Numeric)
|
|
123
|
+
|
|
124
|
+
Float(value.to_s)
|
|
125
|
+
when :boolean
|
|
126
|
+
return value if [true, false].include?(value)
|
|
127
|
+
|
|
128
|
+
%w[true 1 yes].include?(value.to_s.downcase)
|
|
129
|
+
when :string, :enum
|
|
130
|
+
value.to_s
|
|
131
|
+
when :array, :object
|
|
132
|
+
value
|
|
133
|
+
else
|
|
134
|
+
value
|
|
135
|
+
end
|
|
136
|
+
rescue ArgumentError, TypeError
|
|
137
|
+
raise ToolError, "#{name} must be #{type}, got #{value.inspect}"
|
|
77
138
|
end
|
|
78
139
|
end
|
|
79
140
|
|
|
80
141
|
class ToolError < RCrewAI::Error; end
|
|
81
142
|
end
|
|
82
|
-
end
|
|
143
|
+
end
|
|
@@ -8,12 +8,21 @@ require 'fileutils'
|
|
|
8
8
|
module RCrewAI
|
|
9
9
|
module Tools
|
|
10
10
|
class CodeExecutor < Base
|
|
11
|
+
tool_name 'code_executor'
|
|
12
|
+
description 'Execute code in a sandboxed subprocess'
|
|
13
|
+
param :code, type: :string, required: true, description: 'Source code to run'
|
|
14
|
+
param :language, type: :enum, required: true,
|
|
15
|
+
values: %w[ruby python javascript bash],
|
|
16
|
+
description: 'Language: ruby, python, javascript, or bash'
|
|
17
|
+
param :args, type: :array, required: false, items: { type: :string },
|
|
18
|
+
description: 'Optional command-line arguments to pass to the interpreter'
|
|
19
|
+
param :stdin, type: :string, required: false,
|
|
20
|
+
description: 'Optional stdin payload'
|
|
21
|
+
|
|
11
22
|
def initialize(**options)
|
|
12
23
|
super()
|
|
13
|
-
@name = 'codeexecutor'
|
|
14
|
-
@description = 'Execute code in various programming languages (Python, Ruby, JavaScript, etc.)'
|
|
15
24
|
@timeout = options.fetch(:timeout, 30)
|
|
16
|
-
@max_output_size = options.fetch(:max_output_size, 100_000)
|
|
25
|
+
@max_output_size = options.fetch(:max_output_size, 100_000) # 100KB
|
|
17
26
|
@allowed_languages = options.fetch(:allowed_languages, %w[python ruby javascript bash])
|
|
18
27
|
@working_directory = options[:working_directory] || Dir.mktmpdir('rcrewai_code_')
|
|
19
28
|
@enable_file_operations = options.fetch(:enable_file_operations, false)
|
|
@@ -21,18 +30,18 @@ module RCrewAI
|
|
|
21
30
|
end
|
|
22
31
|
|
|
23
32
|
def execute(**params)
|
|
24
|
-
validate_params!(params, required: [
|
|
25
|
-
|
|
33
|
+
validate_params!(params, required: %i[code language], optional: %i[args stdin])
|
|
34
|
+
|
|
26
35
|
language = params[:language].to_s.downcase
|
|
27
36
|
code = params[:code]
|
|
28
37
|
args = params[:args] || []
|
|
29
38
|
stdin_input = params[:stdin]
|
|
30
|
-
|
|
39
|
+
|
|
31
40
|
begin
|
|
32
41
|
validate_execution_params!(language, code)
|
|
33
42
|
result = execute_code(language, code, args, stdin_input)
|
|
34
43
|
format_execution_result(language, code, result)
|
|
35
|
-
rescue => e
|
|
44
|
+
rescue StandardError => e
|
|
36
45
|
"Code execution failed: #{e.message}"
|
|
37
46
|
end
|
|
38
47
|
end
|
|
@@ -43,7 +52,7 @@ module RCrewAI
|
|
|
43
52
|
# Create secure working directory
|
|
44
53
|
FileUtils.mkdir_p(@working_directory)
|
|
45
54
|
File.chmod(0o755, @working_directory)
|
|
46
|
-
|
|
55
|
+
|
|
47
56
|
# Set environment variables for security
|
|
48
57
|
@secure_env = {
|
|
49
58
|
'PATH' => '/usr/local/bin:/usr/bin:/bin',
|
|
@@ -58,63 +67,57 @@ module RCrewAI
|
|
|
58
67
|
unless @allowed_languages.include?(language)
|
|
59
68
|
raise ToolError, "Language not allowed: #{language}. Allowed: #{@allowed_languages.join(', ')}"
|
|
60
69
|
end
|
|
61
|
-
|
|
62
|
-
if code.to_s.strip.empty?
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
|
|
71
|
+
raise ToolError, 'Code cannot be empty' if code.to_s.strip.empty?
|
|
72
|
+
|
|
73
|
+
raise ToolError, "Code too long: #{code.length} characters (max: 50,000)" if code.length > 50_000 # 50KB max
|
|
74
|
+
|
|
70
75
|
validate_code_safety!(language, code)
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
def validate_code_safety!(language, code)
|
|
74
79
|
code_lower = code.downcase
|
|
75
|
-
|
|
80
|
+
|
|
76
81
|
# Common dangerous patterns across languages
|
|
77
82
|
dangerous_patterns = [
|
|
78
83
|
# System operations
|
|
79
84
|
'system(', 'exec(', 'eval(', 'subprocess', 'os.system',
|
|
80
85
|
'shell_exec', 'passthru', '`', 'require "open3"',
|
|
81
|
-
|
|
86
|
+
|
|
82
87
|
# File operations (if not explicitly enabled)
|
|
83
88
|
'file.open', 'open(', 'with open', 'fopen', 'file_get_contents',
|
|
84
89
|
'file_put_contents', 'unlink', 'delete', 'remove',
|
|
85
|
-
|
|
90
|
+
|
|
86
91
|
# Network operations
|
|
87
92
|
'socket', 'http', 'urllib', 'requests', 'net::',
|
|
88
93
|
'tcpsocket', 'udpsocket', 'curl', 'wget',
|
|
89
|
-
|
|
94
|
+
|
|
90
95
|
# Process operations
|
|
91
96
|
'fork', 'spawn', 'thread', 'process',
|
|
92
|
-
|
|
97
|
+
|
|
93
98
|
# Dangerous Python imports
|
|
94
99
|
'import os', 'import sys', 'import subprocess', 'import socket',
|
|
95
100
|
'from os', 'from sys', 'from subprocess',
|
|
96
|
-
|
|
101
|
+
|
|
97
102
|
# Dangerous Ruby requires
|
|
98
103
|
'require "fileutils"', 'require "open3"', 'require "net/',
|
|
99
|
-
|
|
104
|
+
|
|
100
105
|
# Shell commands
|
|
101
106
|
'rm -', 'sudo', 'chmod', 'chown', 'dd if=', 'mkfs',
|
|
102
107
|
'iptables', 'systemctl', 'service '
|
|
103
108
|
]
|
|
104
|
-
|
|
109
|
+
|
|
105
110
|
unless @enable_file_operations
|
|
106
111
|
dangerous_patterns += [
|
|
107
112
|
'file.', 'open(', 'with open', 'fopen', 'file_get_contents',
|
|
108
113
|
'file_put_contents', 'fileutils', 'fs.', 'path.', 'dir.'
|
|
109
114
|
]
|
|
110
115
|
end
|
|
111
|
-
|
|
116
|
+
|
|
112
117
|
dangerous_patterns.each do |pattern|
|
|
113
|
-
if code_lower.include?(pattern)
|
|
114
|
-
raise ToolError, "Potentially dangerous code detected: #{pattern}"
|
|
115
|
-
end
|
|
118
|
+
raise ToolError, "Potentially dangerous code detected: #{pattern}" if code_lower.include?(pattern)
|
|
116
119
|
end
|
|
117
|
-
|
|
120
|
+
|
|
118
121
|
# Language-specific checks
|
|
119
122
|
case language
|
|
120
123
|
when 'python'
|
|
@@ -135,11 +138,9 @@ module RCrewAI
|
|
|
135
138
|
'compile(', 'exec(', 'eval(',
|
|
136
139
|
'input(', 'raw_input('
|
|
137
140
|
]
|
|
138
|
-
|
|
141
|
+
|
|
139
142
|
python_dangerous.each do |pattern|
|
|
140
|
-
if code.include?(pattern)
|
|
141
|
-
raise ToolError, "Dangerous Python construct: #{pattern}"
|
|
142
|
-
end
|
|
143
|
+
raise ToolError, "Dangerous Python construct: #{pattern}" if code.include?(pattern)
|
|
143
144
|
end
|
|
144
145
|
end
|
|
145
146
|
|
|
@@ -150,11 +151,9 @@ module RCrewAI
|
|
|
150
151
|
'const_get', 'const_set', 'remove_const',
|
|
151
152
|
'gets', 'readline', 'readlines'
|
|
152
153
|
]
|
|
153
|
-
|
|
154
|
+
|
|
154
155
|
ruby_dangerous.each do |pattern|
|
|
155
|
-
if code.include?(pattern)
|
|
156
|
-
raise ToolError, "Dangerous Ruby construct: #{pattern}"
|
|
157
|
-
end
|
|
156
|
+
raise ToolError, "Dangerous Ruby construct: #{pattern}" if code.include?(pattern)
|
|
158
157
|
end
|
|
159
158
|
end
|
|
160
159
|
|
|
@@ -165,11 +164,9 @@ module RCrewAI
|
|
|
165
164
|
'document.', 'window.', 'location.',
|
|
166
165
|
'settimeout', 'setinterval'
|
|
167
166
|
]
|
|
168
|
-
|
|
167
|
+
|
|
169
168
|
js_dangerous.each do |pattern|
|
|
170
|
-
if code.include?(pattern)
|
|
171
|
-
raise ToolError, "Dangerous JavaScript construct: #{pattern}"
|
|
172
|
-
end
|
|
169
|
+
raise ToolError, "Dangerous JavaScript construct: #{pattern}" if code.include?(pattern)
|
|
173
170
|
end
|
|
174
171
|
end
|
|
175
172
|
|
|
@@ -180,11 +177,9 @@ module RCrewAI
|
|
|
180
177
|
'$(', '`', 'eval ', 'exec ',
|
|
181
178
|
'/etc/', '/proc/', '/sys/', '/dev/'
|
|
182
179
|
]
|
|
183
|
-
|
|
180
|
+
|
|
184
181
|
bash_dangerous.each do |pattern|
|
|
185
|
-
if code.include?(pattern)
|
|
186
|
-
raise ToolError, "Dangerous bash construct: #{pattern}"
|
|
187
|
-
end
|
|
182
|
+
raise ToolError, "Dangerous bash construct: #{pattern}" if code.include?(pattern)
|
|
188
183
|
end
|
|
189
184
|
end
|
|
190
185
|
|
|
@@ -240,15 +235,15 @@ module RCrewAI
|
|
|
240
235
|
temp_file.close
|
|
241
236
|
yield temp_file.path
|
|
242
237
|
ensure
|
|
243
|
-
temp_file
|
|
238
|
+
temp_file&.unlink
|
|
244
239
|
end
|
|
245
240
|
end
|
|
246
241
|
|
|
247
242
|
def execute_command(command, stdin_input)
|
|
248
|
-
stdout_str =
|
|
249
|
-
stderr_str =
|
|
243
|
+
stdout_str = ''
|
|
244
|
+
stderr_str = ''
|
|
250
245
|
exit_status = nil
|
|
251
|
-
|
|
246
|
+
|
|
252
247
|
# Execute with timeout and security restrictions
|
|
253
248
|
Open3.popen3(@secure_env, *command, chdir: @working_directory) do |stdin, stdout, stderr, wait_thread|
|
|
254
249
|
# Write stdin if provided
|
|
@@ -256,41 +251,45 @@ module RCrewAI
|
|
|
256
251
|
stdin.write(stdin_input)
|
|
257
252
|
stdin.close
|
|
258
253
|
end
|
|
259
|
-
|
|
254
|
+
|
|
260
255
|
# Set up timeout
|
|
261
256
|
timeout_thread = Thread.new do
|
|
262
257
|
sleep @timeout
|
|
263
|
-
|
|
258
|
+
begin
|
|
259
|
+
Process.kill('KILL', wait_thread.pid)
|
|
260
|
+
rescue StandardError
|
|
261
|
+
nil
|
|
262
|
+
end
|
|
264
263
|
end
|
|
265
|
-
|
|
264
|
+
|
|
266
265
|
begin
|
|
267
266
|
# Read output
|
|
268
267
|
stdout_str = read_with_limit(stdout, @max_output_size)
|
|
269
268
|
stderr_str = read_with_limit(stderr, @max_output_size)
|
|
270
|
-
|
|
269
|
+
|
|
271
270
|
exit_status = wait_thread.value.exitstatus
|
|
272
|
-
rescue => e
|
|
271
|
+
rescue StandardError => e
|
|
273
272
|
exit_status = -1
|
|
274
273
|
stderr_str += "\nExecution error: #{e.message}"
|
|
275
274
|
ensure
|
|
276
275
|
timeout_thread.kill
|
|
277
276
|
end
|
|
278
277
|
end
|
|
279
|
-
|
|
278
|
+
|
|
280
279
|
{
|
|
281
280
|
stdout: stdout_str,
|
|
282
281
|
stderr: stderr_str,
|
|
283
282
|
exit_status: exit_status,
|
|
284
|
-
success: exit_status
|
|
283
|
+
success: exit_status.zero?
|
|
285
284
|
}
|
|
286
285
|
end
|
|
287
286
|
|
|
288
287
|
def read_with_limit(io, limit)
|
|
289
|
-
output =
|
|
288
|
+
output = ''
|
|
290
289
|
while output.bytesize < limit && !io.eof?
|
|
291
290
|
chunk = io.read(1024)
|
|
292
291
|
break unless chunk
|
|
293
|
-
|
|
292
|
+
|
|
294
293
|
if output.bytesize + chunk.bytesize > limit
|
|
295
294
|
remaining = limit - output.bytesize
|
|
296
295
|
output += chunk[0, remaining]
|
|
@@ -303,31 +302,29 @@ module RCrewAI
|
|
|
303
302
|
output
|
|
304
303
|
end
|
|
305
304
|
|
|
306
|
-
def format_execution_result(language,
|
|
305
|
+
def format_execution_result(language, _code, result)
|
|
307
306
|
output = []
|
|
308
307
|
output << "Code Execution Result (#{language.upcase})"
|
|
309
|
-
output <<
|
|
308
|
+
output << '=' * 40
|
|
310
309
|
output << "Exit Status: #{result[:exit_status]} (#{result[:success] ? 'SUCCESS' : 'FAILURE'})"
|
|
311
|
-
output <<
|
|
312
|
-
|
|
310
|
+
output << ''
|
|
311
|
+
|
|
313
312
|
if result[:stdout] && !result[:stdout].empty?
|
|
314
|
-
output <<
|
|
313
|
+
output << 'STDOUT:'
|
|
315
314
|
output << result[:stdout]
|
|
316
|
-
output <<
|
|
315
|
+
output << ''
|
|
317
316
|
end
|
|
318
|
-
|
|
317
|
+
|
|
319
318
|
if result[:stderr] && !result[:stderr].empty?
|
|
320
|
-
output <<
|
|
319
|
+
output << 'STDERR:'
|
|
321
320
|
output << result[:stderr]
|
|
322
|
-
output <<
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
if result[:stdout].empty? && result[:stderr].empty?
|
|
326
|
-
output << "No output produced."
|
|
321
|
+
output << ''
|
|
327
322
|
end
|
|
328
|
-
|
|
323
|
+
|
|
324
|
+
output << 'No output produced.' if result[:stdout].empty? && result[:stderr].empty?
|
|
325
|
+
|
|
329
326
|
output.join("\n")
|
|
330
327
|
end
|
|
331
328
|
end
|
|
332
329
|
end
|
|
333
|
-
end
|
|
330
|
+
end
|