roast-ai 0.4.0 → 0.4.2
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/.github/workflows/ci.yaml +2 -2
- data/.gitignore +1 -0
- data/CHANGELOG.md +103 -0
- data/CLAUDE.md +55 -9
- data/Gemfile.lock +19 -10
- data/README.md +69 -3
- data/bin/console +1 -0
- data/docs/AGENT_STEPS.md +33 -9
- data/docs/VALIDATION.md +178 -0
- data/examples/agent_continue/add_documentation/prompt.md +5 -0
- data/examples/agent_continue/add_error_handling/prompt.md +5 -0
- data/examples/agent_continue/analyze_codebase/prompt.md +7 -0
- data/examples/agent_continue/combined_workflow.yml +24 -0
- data/examples/agent_continue/continue_adding_features/prompt.md +4 -0
- data/examples/agent_continue/create_integration_tests/prompt.md +3 -0
- data/examples/agent_continue/document_with_context/prompt.md +5 -0
- data/examples/agent_continue/explore_api/prompt.md +6 -0
- data/examples/agent_continue/implement_client/prompt.md +6 -0
- data/examples/agent_continue/inline_workflow.yml +20 -0
- data/examples/agent_continue/refactor_code/prompt.md +2 -0
- data/examples/agent_continue/verify_changes/prompt.md +6 -0
- data/examples/agent_continue/workflow.yml +27 -0
- data/examples/agent_workflow/workflow.png +0 -0
- data/examples/api_workflow/workflow.png +0 -0
- data/examples/apply_diff_demo/README.md +58 -0
- data/examples/apply_diff_demo/apply_simple_change/prompt.md +13 -0
- data/examples/apply_diff_demo/create_sample_file/prompt.md +11 -0
- data/examples/apply_diff_demo/workflow.yml +24 -0
- data/examples/available_tools_demo/workflow.png +0 -0
- data/examples/bash_prototyping/api_testing.png +0 -0
- data/examples/bash_prototyping/system_analysis.png +0 -0
- data/examples/case_when/workflow.png +0 -0
- data/examples/cmd/basic_workflow.png +0 -0
- data/examples/cmd/dev_workflow.png +0 -0
- data/examples/cmd/explorer_workflow.png +0 -0
- data/examples/conditional/simple_workflow.png +0 -0
- data/examples/conditional/workflow.png +0 -0
- data/examples/context_management_demo/README.md +43 -0
- data/examples/context_management_demo/workflow.yml +42 -0
- data/examples/direct_coerce_syntax/workflow.png +0 -0
- data/examples/dot_notation/workflow.png +0 -0
- data/examples/exit_on_error/workflow.png +0 -0
- data/examples/grading/rb_test_runner +1 -1
- data/examples/grading/workflow.png +0 -0
- data/examples/interpolation/workflow.png +0 -0
- data/examples/interpolation/workflow.yml +1 -1
- data/examples/iteration/workflow.png +0 -0
- data/examples/json_handling/workflow.png +0 -0
- data/examples/mcp/database_workflow.png +0 -0
- data/examples/mcp/env_demo/workflow.png +0 -0
- data/examples/mcp/filesystem_demo/workflow.png +0 -0
- data/examples/mcp/github_workflow.png +0 -0
- data/examples/mcp/multi_mcp_workflow.png +0 -0
- data/examples/mcp/workflow.png +0 -0
- data/examples/no_model_fallback/README.md +17 -0
- data/examples/no_model_fallback/analyze_file/prompt.md +1 -0
- data/examples/no_model_fallback/analyze_patterns/prompt.md +27 -0
- data/examples/no_model_fallback/generate_report_for_md/prompt.md +10 -0
- data/examples/no_model_fallback/generate_report_for_rb/prompt.md +3 -0
- data/examples/no_model_fallback/sample.rb +42 -0
- data/examples/no_model_fallback/workflow.yml +19 -0
- data/examples/openrouter_example/workflow.png +0 -0
- data/examples/pre_post_processing/workflow.png +0 -0
- data/examples/rspec_to_minitest/workflow.png +0 -0
- data/examples/shared_config/example_with_shared_config/workflow.png +0 -0
- data/examples/shared_config/shared.png +0 -0
- data/examples/single_target_prepost/workflow.png +0 -0
- data/examples/smart_coercion_defaults/workflow.png +0 -0
- data/examples/step_configuration/workflow.png +0 -0
- data/examples/swarm_example.yml +25 -0
- data/examples/tool_config_example/workflow.png +0 -0
- data/examples/user_input/funny_name/workflow.png +0 -0
- data/examples/user_input/simple_input_demo/workflow.png +0 -0
- data/examples/user_input/survey_workflow.png +0 -0
- data/examples/user_input/workflow.png +0 -0
- data/examples/workflow_generator/workflow.png +0 -0
- data/lib/roast/errors.rb +3 -0
- data/lib/roast/helpers/timeout_handler.rb +91 -0
- data/lib/roast/services/context_threshold_checker.rb +42 -0
- data/lib/roast/services/token_counting_service.rb +44 -0
- data/lib/roast/tools/apply_diff.rb +128 -0
- data/lib/roast/tools/bash.rb +15 -9
- data/lib/roast/tools/cmd.rb +32 -12
- data/lib/roast/tools/coding_agent.rb +65 -10
- data/lib/roast/tools/context_summarizer.rb +108 -0
- data/lib/roast/tools/swarm.rb +124 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/agent_step.rb +9 -2
- data/lib/roast/workflow/base_iteration_step.rb +3 -2
- data/lib/roast/workflow/base_workflow.rb +41 -2
- data/lib/roast/workflow/command_executor.rb +3 -1
- data/lib/roast/workflow/configuration.rb +2 -1
- data/lib/roast/workflow/configuration_loader.rb +63 -1
- data/lib/roast/workflow/configuration_parser.rb +2 -0
- data/lib/roast/workflow/context_manager.rb +89 -0
- data/lib/roast/workflow/each_step.rb +1 -1
- data/lib/roast/workflow/input_step.rb +2 -0
- data/lib/roast/workflow/interpolator.rb +23 -1
- data/lib/roast/workflow/output_handler.rb +1 -1
- data/lib/roast/workflow/repeat_step.rb +1 -1
- data/lib/roast/workflow/replay_handler.rb +1 -1
- data/lib/roast/workflow/sqlite_state_repository.rb +342 -0
- data/lib/roast/workflow/state_manager.rb +2 -2
- data/lib/roast/workflow/state_repository_factory.rb +36 -0
- data/lib/roast/workflow/step_completion_reporter.rb +27 -0
- data/lib/roast/workflow/step_executor_coordinator.rb +19 -18
- data/lib/roast/workflow/step_executor_with_reporting.rb +68 -0
- data/lib/roast/workflow/step_loader.rb +1 -1
- data/lib/roast/workflow/step_name_extractor.rb +84 -0
- data/lib/roast/workflow/validation_command.rb +197 -0
- data/lib/roast/workflow/validators/base_validator.rb +44 -0
- data/lib/roast/workflow/validators/dependency_validator.rb +223 -0
- data/lib/roast/workflow/validators/linting_validator.rb +113 -0
- data/lib/roast/workflow/validators/schema_validator.rb +90 -0
- data/lib/roast/workflow/validators/step_collector.rb +57 -0
- data/lib/roast/workflow/validators/validation_orchestrator.rb +52 -0
- data/lib/roast/workflow/workflow_executor.rb +11 -4
- data/lib/roast/workflow/workflow_initializer.rb +80 -0
- data/lib/roast/workflow/workflow_runner.rb +6 -0
- data/lib/roast/workflow_diagram_generator.rb +298 -0
- data/lib/roast.rb +158 -0
- data/roast.gemspec +4 -1
- data/schema/workflow.json +77 -1
- metadata +129 -1
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Sample Ruby file for testing interpolation in workflows
|
4
|
+
|
5
|
+
class Calculator
|
6
|
+
def initialize
|
7
|
+
@memory = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(number)
|
11
|
+
@memory += number
|
12
|
+
end
|
13
|
+
|
14
|
+
def subtract(number)
|
15
|
+
@memory -= number
|
16
|
+
end
|
17
|
+
|
18
|
+
def multiply(number)
|
19
|
+
@memory *= number
|
20
|
+
end
|
21
|
+
|
22
|
+
def divide(number)
|
23
|
+
raise "Division by zero!" if number.zero?
|
24
|
+
|
25
|
+
@memory /= number
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :memory
|
29
|
+
|
30
|
+
def clear
|
31
|
+
@memory = 0
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Example usage
|
36
|
+
if __FILE__ == $PROGRAM_NAME
|
37
|
+
calc = Calculator.new
|
38
|
+
calc.add(10)
|
39
|
+
calc.multiply(2)
|
40
|
+
calc.subtract(5)
|
41
|
+
puts "Result: #{calc.memory}"
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
name: no_model_fallback_example
|
2
|
+
|
3
|
+
tools:
|
4
|
+
- Roast::Tools::ReadFile
|
5
|
+
|
6
|
+
steps:
|
7
|
+
- analyze_file
|
8
|
+
- analyze_patterns
|
9
|
+
- generate_report_for_{{File.extname(workflow.file).sub('.', '')}}
|
10
|
+
- '$(echo "Processing completed for file: {{File.basename(workflow.file)}}")'
|
11
|
+
|
12
|
+
analyze_patterns:
|
13
|
+
json: true
|
14
|
+
|
15
|
+
generate_report_for_rb:
|
16
|
+
print_response: true
|
17
|
+
|
18
|
+
generate_report_for_md:
|
19
|
+
print_response: true
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,25 @@
|
|
1
|
+
name: Swarm Example Workflow
|
2
|
+
|
3
|
+
# Example workflow demonstrating Roast's integration with Claude Swarm
|
4
|
+
# The Swarm tool is available to the LLM, which can choose to use it when appropriate
|
5
|
+
|
6
|
+
tools:
|
7
|
+
- Roast::Tools::Swarm:
|
8
|
+
path: ".swarm.yml" # Optional - will use default locations if not specified
|
9
|
+
|
10
|
+
steps:
|
11
|
+
- orchestrate_refactoring: |
|
12
|
+
Help me refactor this codebase for better performance. Coordinate multiple
|
13
|
+
Claude agents using the swarm configuration to:
|
14
|
+
1. Analyze the current code structure
|
15
|
+
2. Identify performance bottlenecks
|
16
|
+
3. Implement optimizations
|
17
|
+
4. Ensure backward compatibility
|
18
|
+
|
19
|
+
- specialized_analysis: |
|
20
|
+
Now use the specialized swarm configuration at ./specialized-swarm.yml to run
|
21
|
+
a comprehensive code analysis that includes:
|
22
|
+
- Architecture review
|
23
|
+
- Security audit
|
24
|
+
- Documentation generation
|
25
|
+
- Test coverage analysis
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/lib/roast/errors.rb
CHANGED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "open3"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Helpers
|
8
|
+
# Shared timeout handling logic for command-based tools
|
9
|
+
#
|
10
|
+
# This class provides centralized timeout functionality for executing shell commands
|
11
|
+
# with proper process management and resource cleanup.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# output, status = TimeoutHandler.call("echo hello", timeout: 5)
|
15
|
+
#
|
16
|
+
# @example With custom working directory
|
17
|
+
# output, status = TimeoutHandler.call("pwd", timeout: 10, working_directory: "/tmp")
|
18
|
+
class TimeoutHandler
|
19
|
+
DEFAULT_TIMEOUT = 30
|
20
|
+
MAX_TIMEOUT = 300
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# Execute a command with timeout using Open3 with proper process cleanup
|
24
|
+
# @param command [String] The command to execute
|
25
|
+
# @param timeout [Integer] Timeout in seconds
|
26
|
+
# @param working_directory [String] Directory to execute in (default: Dir.pwd)
|
27
|
+
# @return [Array<String, Integer>] [output, exit_status]
|
28
|
+
# @raise [Timeout::Error] When command exceeds timeout duration
|
29
|
+
def call(command, timeout: DEFAULT_TIMEOUT, working_directory: Dir.pwd)
|
30
|
+
timeout = validate_timeout(timeout)
|
31
|
+
output = ""
|
32
|
+
exit_status = nil
|
33
|
+
wait_thr = nil
|
34
|
+
|
35
|
+
begin
|
36
|
+
Timeout.timeout(timeout) do
|
37
|
+
stdin, stdout, stderr, wait_thr = Open3.popen3(command, chdir: working_directory)
|
38
|
+
stdin.close # Prevent hanging on stdin-waiting commands
|
39
|
+
output = stdout.read + stderr.read
|
40
|
+
wait_thr.join
|
41
|
+
exit_status = wait_thr.value.exitstatus
|
42
|
+
|
43
|
+
[stdout, stderr].each(&:close)
|
44
|
+
end
|
45
|
+
rescue Timeout::Error
|
46
|
+
# Clean up any remaining processes to prevent zombies
|
47
|
+
cleanup_process(wait_thr) if wait_thr&.alive?
|
48
|
+
raise Timeout::Error, "Command '#{command}' in '#{working_directory}' timed out after #{timeout} seconds"
|
49
|
+
end
|
50
|
+
|
51
|
+
[output, exit_status]
|
52
|
+
end
|
53
|
+
|
54
|
+
# Validate and normalize timeout value
|
55
|
+
# @param timeout [Integer, nil] Raw timeout value
|
56
|
+
# @return [Integer] Validated timeout between 1 and MAX_TIMEOUT
|
57
|
+
def validate_timeout(timeout)
|
58
|
+
return DEFAULT_TIMEOUT if timeout.nil? || timeout <= 0
|
59
|
+
|
60
|
+
[timeout, MAX_TIMEOUT].min
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Clean up process on timeout to prevent zombie processes
|
66
|
+
# @param wait_thr [Process::Waiter] The process thread to clean up
|
67
|
+
def cleanup_process(wait_thr)
|
68
|
+
return unless wait_thr&.alive?
|
69
|
+
|
70
|
+
pid = wait_thr.pid
|
71
|
+
# First try graceful termination
|
72
|
+
Process.kill("TERM", pid)
|
73
|
+
sleep(0.1)
|
74
|
+
|
75
|
+
# Force kill if still alive
|
76
|
+
if wait_thr.alive?
|
77
|
+
Process.kill("KILL", pid)
|
78
|
+
end
|
79
|
+
rescue Errno::ESRCH
|
80
|
+
# Process already terminated, which is fine
|
81
|
+
rescue Errno::EPERM
|
82
|
+
# Permission denied - process may be owned by different user
|
83
|
+
Roast::Helpers::Logger.debug("Could not kill process #{pid}: Permission denied")
|
84
|
+
rescue => e
|
85
|
+
# Catch any other unexpected errors during cleanup
|
86
|
+
Roast::Helpers::Logger.debug("Unexpected error during process cleanup: #{e.message}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Services
|
5
|
+
class ContextThresholdChecker
|
6
|
+
# Default max tokens if not specified (128k for GPT-4)
|
7
|
+
DEFAULT_MAX_TOKENS = 128_000
|
8
|
+
|
9
|
+
# Warning threshold as percentage of compaction threshold
|
10
|
+
WARNING_THRESHOLD_RATIO = 0.9
|
11
|
+
|
12
|
+
# Critical threshold as percentage of max tokens
|
13
|
+
CRITICAL_THRESHOLD_RATIO = 0.95
|
14
|
+
|
15
|
+
def should_compact?(token_count, threshold, max_tokens)
|
16
|
+
max_tokens ||= DEFAULT_MAX_TOKENS
|
17
|
+
token_count >= (max_tokens * threshold)
|
18
|
+
end
|
19
|
+
|
20
|
+
def check_warning_threshold(token_count, compaction_threshold, max_tokens)
|
21
|
+
max_tokens ||= DEFAULT_MAX_TOKENS
|
22
|
+
percentage_used = (token_count.to_f / max_tokens * 100).round
|
23
|
+
|
24
|
+
if token_count >= (max_tokens * CRITICAL_THRESHOLD_RATIO)
|
25
|
+
{
|
26
|
+
level: :critical,
|
27
|
+
percentage_used: percentage_used,
|
28
|
+
tokens_used: token_count,
|
29
|
+
max_tokens: max_tokens,
|
30
|
+
}
|
31
|
+
elsif token_count >= (max_tokens * compaction_threshold * WARNING_THRESHOLD_RATIO)
|
32
|
+
{
|
33
|
+
level: :approaching_limit,
|
34
|
+
percentage_used: percentage_used,
|
35
|
+
tokens_used: token_count,
|
36
|
+
max_tokens: max_tokens,
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Services
|
5
|
+
class TokenCountingService
|
6
|
+
# Approximate character-to-token ratio for English text
|
7
|
+
# Based on OpenAI's rule of thumb: ~4 characters per token
|
8
|
+
CHARS_PER_TOKEN = 4.0
|
9
|
+
|
10
|
+
# Base token overhead for message structure
|
11
|
+
MESSAGE_OVERHEAD_TOKENS = 3
|
12
|
+
|
13
|
+
def count_messages(messages)
|
14
|
+
return 0 if messages.nil? || messages.empty?
|
15
|
+
|
16
|
+
messages.sum do |message|
|
17
|
+
count_message(message)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def count_message(message)
|
24
|
+
return 0 if message.nil?
|
25
|
+
|
26
|
+
role_tokens = estimate_tokens(message[:role].to_s)
|
27
|
+
content_tokens = estimate_tokens(message[:content].to_s)
|
28
|
+
|
29
|
+
# Don't add overhead for empty messages
|
30
|
+
return 0 if role_tokens == 0 && content_tokens == 0
|
31
|
+
|
32
|
+
# Add overhead for message structure and special tokens
|
33
|
+
role_tokens + content_tokens + MESSAGE_OVERHEAD_TOKENS
|
34
|
+
end
|
35
|
+
|
36
|
+
def estimate_tokens(text)
|
37
|
+
return 0 if text.nil? || text.empty?
|
38
|
+
|
39
|
+
# Simple character-based estimation
|
40
|
+
(text.length / CHARS_PER_TOKEN).ceil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "cli/ui"
|
4
|
+
|
5
|
+
module Roast
|
6
|
+
module Tools
|
7
|
+
module ApplyDiff
|
8
|
+
extend self
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def included(base)
|
12
|
+
base.class_eval do
|
13
|
+
function(
|
14
|
+
:apply_diff,
|
15
|
+
"Show a diff to the user and apply changes based on their yes/no response",
|
16
|
+
file_path: { type: "string", description: "Path to the file to modify" },
|
17
|
+
old_content: { type: "string", description: "The current content to be replaced" },
|
18
|
+
new_content: { type: "string", description: "The new content to replace with" },
|
19
|
+
description: { type: "string", description: "Optional description of the change", required: false },
|
20
|
+
) do |params|
|
21
|
+
Roast::Tools::ApplyDiff.call(
|
22
|
+
params[:file_path],
|
23
|
+
params[:old_content],
|
24
|
+
params[:new_content],
|
25
|
+
params[:description],
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(file_path, old_content, new_content, description = nil)
|
33
|
+
unless File.exist?(file_path)
|
34
|
+
error_msg = "File not found: #{file_path}"
|
35
|
+
Roast::Helpers::Logger.error(error_msg + "\n")
|
36
|
+
return error_msg
|
37
|
+
end
|
38
|
+
|
39
|
+
current_content = File.read(file_path)
|
40
|
+
unless current_content.include?(old_content)
|
41
|
+
error_msg = "Old content not found in file: #{file_path}"
|
42
|
+
Roast::Helpers::Logger.error(error_msg + "\n")
|
43
|
+
return error_msg
|
44
|
+
end
|
45
|
+
|
46
|
+
# Show the diff
|
47
|
+
show_diff(file_path, old_content, new_content, description)
|
48
|
+
|
49
|
+
# Ask for confirmation
|
50
|
+
prompt_text = "Apply this change? (y/n)"
|
51
|
+
response = ::CLI::UI::Prompt.ask(prompt_text)
|
52
|
+
|
53
|
+
if response.to_s.downcase.start_with?("y")
|
54
|
+
# Apply the change
|
55
|
+
updated_content = current_content.gsub(old_content, new_content)
|
56
|
+
File.write(file_path, updated_content)
|
57
|
+
|
58
|
+
success_msg = "✅ Changes applied to #{file_path}"
|
59
|
+
Roast::Helpers::Logger.info(success_msg + "\n")
|
60
|
+
success_msg
|
61
|
+
else
|
62
|
+
cancel_msg = "❌ Changes cancelled for #{file_path}"
|
63
|
+
Roast::Helpers::Logger.info(cancel_msg + "\n")
|
64
|
+
cancel_msg
|
65
|
+
end
|
66
|
+
rescue StandardError => e
|
67
|
+
error_message = "Error applying diff: #{e.message}"
|
68
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
69
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
70
|
+
error_message
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def show_diff(file_path, old_content, new_content, description)
|
76
|
+
require "tmpdir"
|
77
|
+
|
78
|
+
Roast::Helpers::Logger.info("📝 Proposed change for #{file_path}:\n")
|
79
|
+
|
80
|
+
if description
|
81
|
+
Roast::Helpers::Logger.info("Description: #{description}\n\n")
|
82
|
+
end
|
83
|
+
|
84
|
+
# Create temporary files for git diff
|
85
|
+
Dir.mktmpdir do |tmpdir|
|
86
|
+
# Write current content with old_content replaced by new_content
|
87
|
+
current_content = File.read(file_path)
|
88
|
+
updated_content = current_content.gsub(old_content, new_content)
|
89
|
+
|
90
|
+
# Create temp file with the proposed changes
|
91
|
+
temp_file = File.join(tmpdir, File.basename(file_path))
|
92
|
+
File.write(temp_file, updated_content)
|
93
|
+
|
94
|
+
# Run git diff
|
95
|
+
diff_output = %x(git diff --no-index --no-prefix "#{file_path}" "#{temp_file}" 2>/dev/null)
|
96
|
+
|
97
|
+
if diff_output.empty?
|
98
|
+
Roast::Helpers::Logger.info("No differences found (files are identical)\n")
|
99
|
+
else
|
100
|
+
# Clean up the diff output - remove temp file paths and use relative paths with colors
|
101
|
+
cleaned_diff = diff_output.lines.map do |line|
|
102
|
+
case line
|
103
|
+
when /^diff --git /
|
104
|
+
::CLI::UI.fmt("{{bold:diff --git a/#{file_path} b/#{file_path}}}")
|
105
|
+
when /^--- /
|
106
|
+
::CLI::UI.fmt("{{red:--- a/#{file_path}}}")
|
107
|
+
when /^\+\+\+ /
|
108
|
+
::CLI::UI.fmt("{{green:+++ b/#{file_path}}}")
|
109
|
+
when /^@@/
|
110
|
+
::CLI::UI.fmt("{{cyan:#{line.chomp}}}")
|
111
|
+
when /^-/
|
112
|
+
::CLI::UI.fmt("{{red:#{line.chomp}}}")
|
113
|
+
when /^\+/
|
114
|
+
::CLI::UI.fmt("{{green:#{line.chomp}}}")
|
115
|
+
else
|
116
|
+
line.chomp
|
117
|
+
end
|
118
|
+
end.join("\n")
|
119
|
+
|
120
|
+
Roast::Helpers::Logger.info("#{cleaned_diff}\n")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
Roast::Helpers::Logger.info("\n")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
data/lib/roast/tools/bash.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "English"
|
4
|
+
require "roast/helpers/logger"
|
5
|
+
require "roast/helpers/timeout_handler"
|
6
|
+
|
3
7
|
module Roast
|
4
8
|
module Tools
|
5
9
|
module Bash
|
@@ -12,14 +16,15 @@ module Roast
|
|
12
16
|
:bash,
|
13
17
|
"Execute any bash command without restrictions. ⚠️ WARNING: Use only in trusted environments!",
|
14
18
|
command: { type: "string", description: "The bash command to execute" },
|
19
|
+
timeout: { type: "integer", description: "Timeout in seconds (optional, default: 30)", required: false },
|
15
20
|
) do |params|
|
16
|
-
Roast::Tools::Bash.call(params[:command])
|
21
|
+
Roast::Tools::Bash.call(params[:command], timeout: params[:timeout])
|
17
22
|
end
|
18
23
|
end
|
19
24
|
end
|
20
25
|
end
|
21
26
|
|
22
|
-
def call(command)
|
27
|
+
def call(command, timeout: 30)
|
23
28
|
Roast::Helpers::Logger.info("🚀 Executing bash command: #{command}\n")
|
24
29
|
|
25
30
|
# Show warning unless explicitly disabled
|
@@ -27,15 +32,16 @@ module Roast
|
|
27
32
|
Roast::Helpers::Logger.warn("⚠️ WARNING: Unrestricted bash execution - use with caution!\n")
|
28
33
|
end
|
29
34
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
exit_status = $CHILD_STATUS.exitstatus
|
35
|
+
result, exit_status = Roast::Helpers::TimeoutHandler.call(
|
36
|
+
"#{command} 2>&1",
|
37
|
+
timeout: timeout,
|
38
|
+
working_directory: Dir.pwd,
|
39
|
+
)
|
37
40
|
|
38
41
|
format_output(command, result, exit_status)
|
42
|
+
rescue Timeout::Error => e
|
43
|
+
Roast::Helpers::Logger.error(e.message + "\n")
|
44
|
+
e.message
|
39
45
|
rescue StandardError => e
|
40
46
|
handle_error(e)
|
41
47
|
end
|
data/lib/roast/tools/cmd.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "English"
|
4
|
+
require "roast/helpers/logger"
|
5
|
+
require "roast/helpers/timeout_handler"
|
6
|
+
|
3
7
|
module Roast
|
4
8
|
module Tools
|
5
9
|
module Cmd
|
@@ -62,6 +66,11 @@ module Roast
|
|
62
66
|
description: "Arguments to pass to the #{command} command",
|
63
67
|
required: false,
|
64
68
|
},
|
69
|
+
timeout: {
|
70
|
+
type: "integer",
|
71
|
+
description: "Timeout in seconds (optional, default: 30)",
|
72
|
+
required: false,
|
73
|
+
},
|
65
74
|
) do |params|
|
66
75
|
full_command = if params[:args].nil? || params[:args].empty?
|
67
76
|
command
|
@@ -69,7 +78,7 @@ module Roast
|
|
69
78
|
"#{command} #{params[:args]}"
|
70
79
|
end
|
71
80
|
|
72
|
-
Roast::Tools::Cmd.execute_allowed_command(full_command, command)
|
81
|
+
Roast::Tools::Cmd.execute_allowed_command(full_command, command, params[:timeout])
|
73
82
|
end
|
74
83
|
end
|
75
84
|
end
|
@@ -80,15 +89,16 @@ module Roast
|
|
80
89
|
end
|
81
90
|
end
|
82
91
|
|
83
|
-
def execute_allowed_command(full_command, command_prefix)
|
92
|
+
def execute_allowed_command(full_command, command_prefix, timeout = 30)
|
84
93
|
Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
|
85
|
-
|
94
|
+
|
95
|
+
execute_command(full_command, command_prefix, timeout)
|
86
96
|
rescue StandardError => e
|
87
97
|
handle_error(e)
|
88
98
|
end
|
89
99
|
|
90
100
|
# Legacy method for backward compatibility
|
91
|
-
def call(command, config = {})
|
101
|
+
def call(command, config = {}, timeout: 30)
|
92
102
|
Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
|
93
103
|
|
94
104
|
allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
|
@@ -96,7 +106,8 @@ module Roast
|
|
96
106
|
return validation_result unless validation_result.nil?
|
97
107
|
|
98
108
|
command_prefix = command.split(" ").first
|
99
|
-
|
109
|
+
|
110
|
+
execute_command(command, command_prefix, timeout)
|
100
111
|
rescue StandardError => e
|
101
112
|
handle_error(e)
|
102
113
|
end
|
@@ -127,16 +138,25 @@ module Roast
|
|
127
138
|
configuration&.tool_config("Roast::Tools::Cmd") || {}
|
128
139
|
end
|
129
140
|
|
130
|
-
def execute_command(command, command_prefix)
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
141
|
+
def execute_command(command, command_prefix, timeout)
|
142
|
+
timeout = Roast::Helpers::TimeoutHandler.validate_timeout(timeout)
|
143
|
+
|
144
|
+
full_command = if command_prefix == "dev"
|
145
|
+
"bash -l -c '#{command.gsub("'", "\\'")}'"
|
135
146
|
else
|
136
|
-
|
147
|
+
command
|
137
148
|
end
|
138
149
|
|
139
|
-
|
150
|
+
result, exit_status = Roast::Helpers::TimeoutHandler.call(
|
151
|
+
full_command,
|
152
|
+
timeout: timeout,
|
153
|
+
working_directory: Dir.pwd,
|
154
|
+
)
|
155
|
+
|
156
|
+
format_output(command, result, exit_status)
|
157
|
+
rescue Timeout::Error => e
|
158
|
+
Roast::Helpers::Logger.error(e.message + "\n")
|
159
|
+
e.message
|
140
160
|
end
|
141
161
|
|
142
162
|
def format_output(command, result, exit_status)
|