roast-ai 0.2.3 → 0.3.1
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/CHANGELOG.md +31 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +15 -10
- data/README.md +214 -20
- data/bin/roast +1 -1
- data/examples/bash_prototyping/README.md +53 -0
- data/examples/bash_prototyping/analyze_network/prompt.md +13 -0
- data/examples/bash_prototyping/analyze_system/prompt.md +11 -0
- data/examples/bash_prototyping/api_testing.yml +14 -0
- data/examples/bash_prototyping/check_processes/prompt.md +11 -0
- data/examples/bash_prototyping/generate_report/prompt.md +16 -0
- data/examples/bash_prototyping/process_json_response/prompt.md +24 -0
- data/examples/bash_prototyping/system_analysis.yml +14 -0
- data/examples/bash_prototyping/test_public_api/prompt.md +22 -0
- data/examples/cmd/README.md +99 -0
- data/examples/cmd/analyze_project/prompt.md +57 -0
- data/examples/cmd/basic_demo/prompt.md +48 -0
- data/examples/cmd/basic_workflow.yml +17 -0
- data/examples/cmd/check_repository/prompt.md +57 -0
- data/examples/cmd/create_and_verify/prompt.md +56 -0
- data/examples/cmd/dev_workflow.yml +26 -0
- data/examples/cmd/explore_project/prompt.md +67 -0
- data/examples/cmd/explorer_workflow.yml +21 -0
- data/examples/cmd/smart_tool_selection/prompt.md +99 -0
- data/examples/grading/README.md +71 -0
- data/examples/grading/read_dependencies/prompt.md +4 -2
- data/examples/grading/run_coverage.rb +9 -0
- data/examples/grading/workflow.yml +0 -2
- data/examples/mcp/README.md +223 -0
- data/examples/mcp/analyze_changes/prompt.md +8 -0
- data/examples/mcp/analyze_issues/prompt.md +4 -0
- data/examples/mcp/analyze_schema/prompt.md +4 -0
- data/examples/mcp/check_data_quality/prompt.md +5 -0
- data/examples/mcp/check_documentation/prompt.md +4 -0
- data/examples/mcp/create_recommendations/prompt.md +5 -0
- data/examples/mcp/database_workflow.yml +29 -0
- data/examples/mcp/env_demo/workflow.yml +34 -0
- data/examples/mcp/fetch_pr_context/prompt.md +4 -0
- data/examples/mcp/filesystem_demo/create_test_file/prompt.md +2 -0
- data/examples/mcp/filesystem_demo/list_files/prompt.md +6 -0
- data/examples/mcp/filesystem_demo/read_with_mcp/prompt.md +7 -0
- data/examples/mcp/filesystem_demo/workflow.yml +38 -0
- data/examples/mcp/generate_insights/prompt.md +4 -0
- data/examples/mcp/generate_report/prompt.md +6 -0
- data/examples/mcp/generate_review/prompt.md +16 -0
- data/examples/mcp/github_workflow.yml +32 -0
- data/examples/mcp/multi_mcp_workflow.yml +58 -0
- data/examples/mcp/post_review/prompt.md +3 -0
- data/examples/mcp/save_report/prompt.md +6 -0
- data/examples/mcp/search_issues/prompt.md +2 -0
- data/examples/mcp/summarize/prompt.md +1 -0
- data/examples/mcp/test_filesystem/prompt.md +6 -0
- data/examples/mcp/test_github/prompt.md +8 -0
- data/examples/mcp/test_read/prompt.md +1 -0
- data/examples/mcp/workflow.yml +35 -0
- data/examples/shared_config/README.md +52 -0
- data/examples/shared_config/example_with_shared_config/workflow.yml +6 -0
- data/examples/shared_config/shared.yml +7 -0
- data/examples/step_configuration/README.md +0 -3
- data/examples/step_configuration/workflow.yml +0 -3
- data/examples/tool_config_example/README.md +109 -0
- data/examples/tool_config_example/example_step/prompt.md +42 -0
- data/examples/tool_config_example/workflow.yml +17 -0
- data/examples/workflow_generator/workflow.yml +0 -1
- data/lib/roast/helpers/function_caching_interceptor.rb +0 -4
- data/lib/roast/helpers/prompt_loader.rb +0 -1
- data/lib/roast/tools/bash.rb +62 -0
- data/lib/roast/tools/cmd.rb +121 -34
- data/lib/roast/tools/coding_agent.rb +86 -7
- data/lib/roast/tools/helpers/coding_agent_message_formatter.rb +87 -0
- data/lib/roast/tools/search_file.rb +13 -1
- data/lib/roast/tools.rb +5 -5
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_step.rb +29 -21
- data/lib/roast/workflow/base_workflow.rb +8 -10
- data/lib/roast/workflow/configuration.rb +68 -3
- data/lib/roast/workflow/configuration_loader.rb +63 -4
- data/lib/roast/workflow/configuration_parser.rb +0 -3
- data/lib/roast/workflow/error_handler.rb +0 -1
- data/lib/roast/workflow/file_state_repository.rb +0 -1
- data/lib/roast/workflow/iteration_executor.rb +0 -1
- data/lib/roast/workflow/output_manager.rb +0 -1
- data/lib/roast/workflow/prompt_step.rb +1 -1
- data/lib/roast/workflow/step_executor_coordinator.rb +5 -3
- data/lib/roast/workflow/step_executors/hash_step_executor.rb +1 -1
- data/lib/roast/workflow/step_loader.rb +35 -8
- data/lib/roast/workflow/step_orchestrator.rb +4 -2
- data/lib/roast/workflow/workflow_execution_context.rb +0 -2
- data/lib/roast/workflow/workflow_executor.rb +1 -3
- data/lib/roast/workflow/workflow_initializer.rb +66 -2
- data/lib/roast/workflow/workflow_runner.rb +1 -2
- data/lib/roast.rb +8 -0
- data/package-lock.json +6 -0
- data/roast.gemspec +2 -1
- metadata +73 -3
@@ -10,7 +10,6 @@ This example demonstrates how to configure various step types in Roast workflows
|
|
10
10
|
All step types support the following configuration options:
|
11
11
|
|
12
12
|
- `model`: The AI model to use (e.g., "gpt-4o", "claude-3-opus")
|
13
|
-
- `loop`: Whether to enable auto-looping (true/false)
|
14
13
|
- `print_response`: Whether to print the response to stdout (true/false)
|
15
14
|
- `json`: Whether to expect a JSON response (true/false)
|
16
15
|
- `params`: Additional parameters to pass to the model (e.g., temperature, max_tokens)
|
@@ -30,7 +29,6 @@ Inline prompts can be configured in two ways:
|
|
30
29
|
```yaml
|
31
30
|
analyze the code:
|
32
31
|
model: gpt-4o
|
33
|
-
loop: false
|
34
32
|
print_response: true
|
35
33
|
```
|
36
34
|
|
@@ -52,7 +50,6 @@ each:
|
|
52
50
|
each: "{{files}}"
|
53
51
|
as: file
|
54
52
|
model: gpt-3.5-turbo
|
55
|
-
loop: false
|
56
53
|
steps:
|
57
54
|
- process {{file}}
|
58
55
|
|
@@ -3,12 +3,10 @@ description: Demonstrates how to configure various step types including inline p
|
|
3
3
|
|
4
4
|
# Global configuration that applies to all steps
|
5
5
|
model: openai/gpt-4o-mini
|
6
|
-
loop: true
|
7
6
|
|
8
7
|
# Configuration for specific steps
|
9
8
|
summarize the code:
|
10
9
|
model: claude-3-opus # Override global model
|
11
|
-
loop: false # Disable auto-loop for this step
|
12
10
|
print_response: true # Print the response
|
13
11
|
json: false # Don't expect JSON response
|
14
12
|
params:
|
@@ -25,7 +23,6 @@ each:
|
|
25
23
|
each: "{{files}}"
|
26
24
|
as: file
|
27
25
|
model: gpt-3.5-turbo # Use a faster model for iteration
|
28
|
-
loop: false # Don't loop on each iteration
|
29
26
|
steps:
|
30
27
|
- process {{file}}
|
31
28
|
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Tool Configuration Example
|
2
|
+
|
3
|
+
This example demonstrates how to configure tools with specific settings in Roast workflows.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Starting with this update, Roast supports configuring tools with specific settings directly in the workflow YAML file. This is particularly useful for tools like `Roast::Tools::Cmd` where you might want to restrict which commands can be executed.
|
8
|
+
|
9
|
+
## Configuration Syntax
|
10
|
+
|
11
|
+
Tools can be configured in two ways:
|
12
|
+
|
13
|
+
### 1. Simple String Format (No Configuration)
|
14
|
+
```yaml
|
15
|
+
tools:
|
16
|
+
- Roast::Tools::ReadFile
|
17
|
+
- Roast::Tools::WriteFile
|
18
|
+
- Roast::Tools::Grep
|
19
|
+
```
|
20
|
+
|
21
|
+
### 2. Hash Format (With Configuration)
|
22
|
+
```yaml
|
23
|
+
tools:
|
24
|
+
- Roast::Tools::Cmd:
|
25
|
+
allowed_commands:
|
26
|
+
- ls
|
27
|
+
- pwd
|
28
|
+
- echo
|
29
|
+
```
|
30
|
+
|
31
|
+
### 3. Mixed Format
|
32
|
+
You can mix both formats in the same workflow:
|
33
|
+
|
34
|
+
```yaml
|
35
|
+
tools:
|
36
|
+
- Roast::Tools::ReadFile
|
37
|
+
- Roast::Tools::Cmd:
|
38
|
+
allowed_commands:
|
39
|
+
- ls
|
40
|
+
- pwd
|
41
|
+
- ruby
|
42
|
+
- sed
|
43
|
+
- Roast::Tools::WriteFile
|
44
|
+
- Roast::Tools::SearchFile
|
45
|
+
```
|
46
|
+
|
47
|
+
## Example: Configuring Allowed Commands
|
48
|
+
|
49
|
+
The `Roast::Tools::Cmd` tool now supports an `allowed_commands` configuration that restricts which commands can be executed:
|
50
|
+
|
51
|
+
```yaml
|
52
|
+
tools:
|
53
|
+
- Roast::Tools::Cmd:
|
54
|
+
allowed_commands:
|
55
|
+
- ls
|
56
|
+
- pwd
|
57
|
+
- echo
|
58
|
+
- cat
|
59
|
+
- ruby
|
60
|
+
- rake
|
61
|
+
```
|
62
|
+
|
63
|
+
### Enhanced Command Configuration with Descriptions
|
64
|
+
|
65
|
+
You can also provide custom descriptions for commands to help the LLM understand their purpose:
|
66
|
+
|
67
|
+
```yaml
|
68
|
+
tools:
|
69
|
+
- Roast::Tools::Cmd:
|
70
|
+
allowed_commands:
|
71
|
+
- ls
|
72
|
+
- pwd
|
73
|
+
- name: echo
|
74
|
+
description: "echo command - output text to stdout, supports > for file redirection"
|
75
|
+
- name: cat
|
76
|
+
description: "cat command - display file contents, concatenate files, works with pipes"
|
77
|
+
```
|
78
|
+
|
79
|
+
This mixed format allows you to:
|
80
|
+
- Use simple strings for commands with good default descriptions
|
81
|
+
- Provide custom descriptions for commands that need more context
|
82
|
+
- Help the LLM make better decisions about which command to use
|
83
|
+
|
84
|
+
With this configuration:
|
85
|
+
- ✅ `ls -la` will work
|
86
|
+
- ✅ `echo "Hello World"` will work
|
87
|
+
- ❌ `rm file.txt` will be rejected (not in allowed list)
|
88
|
+
- ❌ `git status` will be rejected (not in allowed list)
|
89
|
+
|
90
|
+
## Default Behavior
|
91
|
+
|
92
|
+
If no configuration is provided for `Roast::Tools::Cmd`, it uses the default allowed commands:
|
93
|
+
- pwd
|
94
|
+
- find
|
95
|
+
- ls
|
96
|
+
- rake
|
97
|
+
- ruby
|
98
|
+
- dev
|
99
|
+
- mkdir
|
100
|
+
|
101
|
+
## Running the Example
|
102
|
+
|
103
|
+
To run this example workflow:
|
104
|
+
|
105
|
+
```bash
|
106
|
+
bin/roast execute examples/tool_config_example/workflow.yml
|
107
|
+
```
|
108
|
+
|
109
|
+
The workflow will validate the tool configuration by executing various commands and demonstrating which ones are allowed and which are rejected based on the configuration.
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Tool Configuration Validation
|
2
|
+
|
3
|
+
Execute the following commands using the cmd tool:
|
4
|
+
|
5
|
+
1. `ls -la`
|
6
|
+
2. `pwd`
|
7
|
+
3. `echo "Hello from configured commands!"`
|
8
|
+
4. `git status`
|
9
|
+
|
10
|
+
RESPONSE FORMAT
|
11
|
+
You must respond in JSON format within <json> XML tags.
|
12
|
+
|
13
|
+
<json>
|
14
|
+
{
|
15
|
+
"commands": [
|
16
|
+
{
|
17
|
+
"command": "ls -la",
|
18
|
+
"exit_status": 0,
|
19
|
+
"output": "total 208\ndrwxr-xr-x@ 31 user staff...",
|
20
|
+
"success": true
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"command": "pwd",
|
24
|
+
"exit_status": 0,
|
25
|
+
"output": "/Users/user/project",
|
26
|
+
"success": true
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"command": "echo \"Hello from configured commands!\"",
|
30
|
+
"exit_status": 0,
|
31
|
+
"output": "Hello from configured commands!",
|
32
|
+
"success": true
|
33
|
+
},
|
34
|
+
{
|
35
|
+
"command": "git status",
|
36
|
+
"exit_status": null,
|
37
|
+
"output": "Error: Command not allowed. Only commands starting with ls, pwd, echo, cat are permitted.",
|
38
|
+
"success": false
|
39
|
+
}
|
40
|
+
]
|
41
|
+
}
|
42
|
+
</json>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
name: Tool Configuration Example
|
2
|
+
model: default
|
3
|
+
tools:
|
4
|
+
- Roast::Tools::ReadFile
|
5
|
+
- Roast::Tools::Cmd:
|
6
|
+
allowed_commands:
|
7
|
+
- ls
|
8
|
+
- pwd
|
9
|
+
- name: echo
|
10
|
+
description: "echo command - output text to stdout, supports > for file redirection"
|
11
|
+
- name: cat
|
12
|
+
description: "cat command - display file contents, concatenate files, works with pipes"
|
13
|
+
- Roast::Tools::WriteFile
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- example_step
|
17
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "English"
|
4
|
+
require "roast/helpers/logger"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Tools
|
8
|
+
module Bash
|
9
|
+
extend self
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def included(base)
|
13
|
+
base.class_eval do
|
14
|
+
function(
|
15
|
+
:bash,
|
16
|
+
"Execute any bash command without restrictions. ⚠️ WARNING: Use only in trusted environments!",
|
17
|
+
command: { type: "string", description: "The bash command to execute" },
|
18
|
+
) do |params|
|
19
|
+
Roast::Tools::Bash.call(params[:command])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(command)
|
26
|
+
Roast::Helpers::Logger.info("🚀 Executing bash command: #{command}\n")
|
27
|
+
|
28
|
+
# Show warning unless explicitly disabled
|
29
|
+
if ENV["ROAST_BASH_WARNINGS"] != "false"
|
30
|
+
Roast::Helpers::Logger.warn("⚠️ WARNING: Unrestricted bash execution - use with caution!\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Execute the command without any restrictions
|
34
|
+
result = ""
|
35
|
+
IO.popen("#{command} 2>&1", chdir: Dir.pwd) do |io|
|
36
|
+
result = io.read
|
37
|
+
end
|
38
|
+
|
39
|
+
exit_status = $CHILD_STATUS.exitstatus
|
40
|
+
|
41
|
+
format_output(command, result, exit_status)
|
42
|
+
rescue StandardError => e
|
43
|
+
handle_error(e)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def format_output(command, result, exit_status)
|
49
|
+
"Command: #{command}\n" \
|
50
|
+
"Exit status: #{exit_status}\n" \
|
51
|
+
"Output:\n#{result}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_error(error)
|
55
|
+
error_message = "Error running command: #{error.message}"
|
56
|
+
Roast::Helpers::Logger.error("#{error_message}\n")
|
57
|
+
Roast::Helpers::Logger.debug("#{error.backtrace.join("\n")}\n") if ENV["DEBUG"]
|
58
|
+
error_message
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/roast/tools/cmd.rb
CHANGED
@@ -8,64 +8,151 @@ module Roast
|
|
8
8
|
module Cmd
|
9
9
|
extend self
|
10
10
|
|
11
|
+
DEFAULT_ALLOWED_COMMANDS = [
|
12
|
+
{ "name" => "pwd", "description" => "pwd command - print current working directory path" },
|
13
|
+
{ "name" => "find", "description" => "find command - search for files/directories using patterns like -name '*.rb'" },
|
14
|
+
{ "name" => "ls", "description" => "ls command - list directory contents with options like -la, -R" },
|
15
|
+
{ "name" => "rake", "description" => "rake command - run Ruby tasks defined in Rakefile" },
|
16
|
+
{ "name" => "ruby", "description" => "ruby command - execute Ruby code or scripts, supports -e for inline code" },
|
17
|
+
{ "name" => "dev", "description" => "Shopify dev CLI - development environment tool with subcommands" },
|
18
|
+
{ "name" => "mkdir", "description" => "mkdir command - create directories, supports -p for parent directories" },
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
CONFIG_ALLOWED_COMMANDS = "allowed_commands"
|
22
|
+
private_constant :DEFAULT_ALLOWED_COMMANDS, :CONFIG_ALLOWED_COMMANDS
|
23
|
+
|
11
24
|
class << self
|
12
25
|
# Add this method to be included in other classes
|
13
26
|
def included(base)
|
27
|
+
@base_class = base
|
28
|
+
end
|
29
|
+
|
30
|
+
# Called after configuration is loaded
|
31
|
+
def post_configuration_setup(base, config = {})
|
32
|
+
allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
|
33
|
+
|
34
|
+
allowed_commands.each do |command_entry|
|
35
|
+
case command_entry
|
36
|
+
when String
|
37
|
+
register_command_function(base, command_entry, nil)
|
38
|
+
when Hash
|
39
|
+
command_name = command_entry["name"] || command_entry[:name]
|
40
|
+
description = command_entry["description"] || command_entry[:description]
|
41
|
+
|
42
|
+
if command_name.nil?
|
43
|
+
raise ArgumentError, "Command configuration must include 'name' field"
|
44
|
+
end
|
45
|
+
|
46
|
+
register_command_function(base, command_name, description)
|
47
|
+
else
|
48
|
+
raise ArgumentError, "Invalid command configuration format: #{command_entry.inspect}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def register_command_function(base, command, custom_description = nil)
|
56
|
+
function_name = command.to_sym
|
57
|
+
description = custom_description || generate_command_description(command)
|
58
|
+
|
14
59
|
base.class_eval do
|
15
60
|
function(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
61
|
+
function_name,
|
62
|
+
description,
|
63
|
+
args: {
|
64
|
+
type: "string",
|
65
|
+
description: "Arguments to pass to the #{command} command",
|
66
|
+
required: false,
|
67
|
+
},
|
20
68
|
) do |params|
|
21
|
-
|
69
|
+
full_command = if params[:args].nil? || params[:args].empty?
|
70
|
+
command
|
71
|
+
else
|
72
|
+
"#{command} #{params[:args]}"
|
73
|
+
end
|
74
|
+
|
75
|
+
Roast::Tools::Cmd.execute_allowed_command(full_command, command)
|
22
76
|
end
|
23
77
|
end
|
24
78
|
end
|
79
|
+
|
80
|
+
def generate_command_description(command)
|
81
|
+
default_cmd = DEFAULT_ALLOWED_COMMANDS.find { |cmd| cmd["name"] == command }
|
82
|
+
default_cmd ? default_cmd["description"] : "Execute the #{command} command"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute_allowed_command(full_command, command_prefix)
|
87
|
+
Roast::Helpers::Logger.info("🔧 Running command: #{full_command}\n")
|
88
|
+
execute_command(full_command, command_prefix)
|
89
|
+
rescue StandardError => e
|
90
|
+
handle_error(e)
|
25
91
|
end
|
26
92
|
|
27
|
-
|
93
|
+
# Legacy method for backward compatibility
|
94
|
+
def call(command, config = {})
|
28
95
|
Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
|
29
96
|
|
30
|
-
|
31
|
-
|
97
|
+
allowed_commands = config[CONFIG_ALLOWED_COMMANDS] || DEFAULT_ALLOWED_COMMANDS
|
98
|
+
validation_result = validate_command(command, allowed_commands)
|
99
|
+
return validation_result unless validation_result.nil?
|
100
|
+
|
32
101
|
command_prefix = command.split(" ").first
|
102
|
+
execute_command(command, command_prefix)
|
103
|
+
rescue StandardError => e
|
104
|
+
handle_error(e)
|
105
|
+
end
|
33
106
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
107
|
+
private
|
108
|
+
|
109
|
+
def validate_command(command, allowed_commands)
|
110
|
+
command_prefix = command.split(" ").first
|
111
|
+
|
112
|
+
# Extract command names from the allowed_commands array
|
113
|
+
allowed_command_names = allowed_commands.map do |cmd_entry|
|
114
|
+
case cmd_entry
|
115
|
+
when String
|
116
|
+
cmd_entry
|
117
|
+
when Hash
|
118
|
+
cmd_entry["name"] || cmd_entry[:name]
|
119
|
+
end
|
120
|
+
end.compact
|
38
121
|
|
39
|
-
|
40
|
-
|
122
|
+
return if allowed_command_names.include?(command_prefix)
|
123
|
+
|
124
|
+
"Error: Command not allowed. Only commands starting with #{allowed_command_names.join(", ")} are permitted."
|
125
|
+
end
|
41
126
|
|
42
|
-
|
43
|
-
|
127
|
+
def extract_tool_config
|
128
|
+
return {} unless respond_to?(:configuration)
|
129
|
+
|
130
|
+
configuration&.tool_config("Roast::Tools::Cmd") || {}
|
131
|
+
end
|
132
|
+
|
133
|
+
def execute_command(command, command_prefix)
|
134
|
+
result = if command_prefix == "dev"
|
44
135
|
# Use bash -l -c to ensure we get a login shell with all environment variables
|
45
136
|
full_command = "bash -l -c '#{command.gsub("'", "\\'")}'"
|
46
|
-
IO.popen(full_command, chdir: Dir.pwd)
|
47
|
-
result = io.read
|
48
|
-
end
|
137
|
+
IO.popen(full_command, chdir: Dir.pwd, &:read)
|
49
138
|
else
|
50
|
-
|
51
|
-
IO.popen(command, chdir: Dir.pwd) do |io|
|
52
|
-
result = io.read
|
53
|
-
end
|
139
|
+
IO.popen(command, chdir: Dir.pwd, &:read)
|
54
140
|
end
|
55
141
|
|
56
|
-
|
142
|
+
format_output(command, result, $CHILD_STATUS.exitstatus)
|
143
|
+
end
|
57
144
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
145
|
+
def format_output(command, result, exit_status)
|
146
|
+
"Command: #{command}\n" \
|
147
|
+
"Exit status: #{exit_status}\n" \
|
148
|
+
"Output:\n#{result}"
|
149
|
+
end
|
62
150
|
|
63
|
-
|
64
|
-
|
65
|
-
"
|
66
|
-
|
67
|
-
|
68
|
-
end
|
151
|
+
def handle_error(error)
|
152
|
+
error_message = "Error running command: #{error.message}"
|
153
|
+
Roast::Helpers::Logger.error("#{error_message}\n")
|
154
|
+
Roast::Helpers::Logger.debug("#{error.backtrace.join("\n")}\n") if ENV["DEBUG"]
|
155
|
+
error_message
|
69
156
|
end
|
70
157
|
end
|
71
158
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "roast/helpers/logger"
|
4
|
+
require "roast/tools/helpers/coding_agent_message_formatter"
|
5
|
+
require "json"
|
4
6
|
require "open3"
|
5
7
|
require "tempfile"
|
6
8
|
require "securerandom"
|
@@ -10,7 +12,16 @@ module Roast
|
|
10
12
|
module CodingAgent
|
11
13
|
extend self
|
12
14
|
|
15
|
+
class CodingAgentError < StandardError; end
|
16
|
+
|
17
|
+
CONFIG_CODING_AGENT_COMMAND = "coding_agent_command"
|
18
|
+
private_constant :CONFIG_CODING_AGENT_COMMAND
|
19
|
+
|
20
|
+
@configured_command = nil
|
21
|
+
|
13
22
|
class << self
|
23
|
+
attr_accessor :configured_command
|
24
|
+
|
14
25
|
def included(base)
|
15
26
|
base.class_eval do
|
16
27
|
function(
|
@@ -22,6 +33,11 @@ module Roast
|
|
22
33
|
end
|
23
34
|
end
|
24
35
|
end
|
36
|
+
|
37
|
+
# Called after configuration is loaded
|
38
|
+
def post_configuration_setup(base, config = {})
|
39
|
+
self.configured_command = config[CONFIG_CODING_AGENT_COMMAND]
|
40
|
+
end
|
25
41
|
end
|
26
42
|
|
27
43
|
def call(prompt)
|
@@ -50,20 +66,83 @@ module Roast
|
|
50
66
|
temp_file.write(prompt)
|
51
67
|
temp_file.close
|
52
68
|
|
53
|
-
# Run Claude Code CLI using the temp file as input
|
54
|
-
|
55
|
-
|
69
|
+
# Run Claude Code CLI using the temp file as input with streaming output
|
70
|
+
expect_json_output = claude_code_command.include?("--output-format stream-json") ||
|
71
|
+
claude_code_command.include?("--output-format json")
|
72
|
+
command = "cat #{temp_file.path} | #{claude_code_command}"
|
73
|
+
result = ""
|
56
74
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
75
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thread|
|
76
|
+
stdin.close
|
77
|
+
if expect_json_output
|
78
|
+
stdout.each_line do |line|
|
79
|
+
json = parse_json(line)
|
80
|
+
next unless json
|
81
|
+
|
82
|
+
handle_intermediate_message(json)
|
83
|
+
result += handle_result(json) || ""
|
84
|
+
end
|
85
|
+
else
|
86
|
+
result = stdout.read
|
87
|
+
end
|
88
|
+
|
89
|
+
status = wait_thread.value
|
90
|
+
if status.success?
|
91
|
+
return result
|
92
|
+
else
|
93
|
+
error_output = stderr.read
|
94
|
+
return "Error running CodingAgent: #{error_output}"
|
95
|
+
end
|
61
96
|
end
|
62
97
|
ensure
|
63
98
|
# Always clean up the temp file
|
64
99
|
temp_file.unlink
|
65
100
|
end
|
66
101
|
end
|
102
|
+
|
103
|
+
def parse_json(line)
|
104
|
+
JSON.parse(line)
|
105
|
+
rescue JSON::ParserError => e
|
106
|
+
Roast::Helpers::Logger.warn("🤖 Error parsing JSON response: #{e}\n")
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_intermediate_message(json)
|
111
|
+
case json["type"]
|
112
|
+
when "assistant", "user"
|
113
|
+
CodingAgentMessageFormatter.format_messages(json).each(&method(:log_message))
|
114
|
+
when "result", "system"
|
115
|
+
# Ignore these message types
|
116
|
+
else
|
117
|
+
Roast::Helpers::Logger.debug("🤖 Encountered unexpected message type: #{json["type"]}\n")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def handle_result(json)
|
122
|
+
if json["type"] == "result"
|
123
|
+
# NOTE: the format of an error response is { "subtype": "success", "is_error": true }
|
124
|
+
if json["is_error"]
|
125
|
+
raise CodingAgentError, json["result"]
|
126
|
+
elsif json["subtype"] == "success"
|
127
|
+
json["result"]
|
128
|
+
else
|
129
|
+
raise CodingAgentError, "CodingAgent did not complete successfully: #{line}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def log_message(text)
|
135
|
+
return if text.blank?
|
136
|
+
|
137
|
+
text = text.lines.map do |line|
|
138
|
+
"\t#{line}"
|
139
|
+
end.join
|
140
|
+
Roast::Helpers::Logger.info("• " + text.chomp + "\n")
|
141
|
+
end
|
142
|
+
|
143
|
+
def claude_code_command
|
144
|
+
CodingAgent.configured_command || ENV["CLAUDE_CODE_COMMAND"] || "claude -p --verbose --output-format stream-json"
|
145
|
+
end
|
67
146
|
end
|
68
147
|
end
|
69
148
|
end
|