roast-ai 0.1.0 → 0.1.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/.github/workflows/cla.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +20 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +3 -4
- data/README.md +418 -4
- data/Rakefile +1 -6
- data/docs/INSTRUMENTATION.md +202 -0
- data/examples/api_workflow/README.md +85 -0
- data/examples/api_workflow/fetch_api_data/prompt.md +10 -0
- data/examples/api_workflow/generate_report/prompt.md +10 -0
- data/examples/api_workflow/prompt.md +10 -0
- data/examples/api_workflow/transform_data/prompt.md +10 -0
- data/examples/api_workflow/workflow.yml +30 -0
- data/examples/grading/workflow.yml +2 -2
- data/examples/instrumentation.rb +76 -0
- data/examples/rspec_to_minitest/README.md +68 -0
- data/examples/rspec_to_minitest/analyze_spec/prompt.md +30 -0
- data/examples/rspec_to_minitest/create_minitest/prompt.md +33 -0
- data/examples/rspec_to_minitest/run_and_improve/prompt.md +35 -0
- data/examples/rspec_to_minitest/workflow.md +10 -0
- data/examples/rspec_to_minitest/workflow.yml +40 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +72 -8
- data/lib/roast/helpers/prompt_loader.rb +2 -0
- data/lib/roast/resources/api_resource.rb +137 -0
- data/lib/roast/resources/base_resource.rb +47 -0
- data/lib/roast/resources/directory_resource.rb +40 -0
- data/lib/roast/resources/file_resource.rb +33 -0
- data/lib/roast/resources/none_resource.rb +29 -0
- data/lib/roast/resources/url_resource.rb +63 -0
- data/lib/roast/resources.rb +100 -0
- data/lib/roast/tools/coding_agent.rb +69 -0
- data/lib/roast/tools.rb +1 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_step.rb +21 -17
- data/lib/roast/workflow/base_workflow.rb +49 -16
- data/lib/roast/workflow/configuration.rb +83 -8
- data/lib/roast/workflow/configuration_parser.rb +171 -3
- data/lib/roast/workflow/file_state_repository.rb +126 -0
- data/lib/roast/workflow/prompt_step.rb +16 -0
- data/lib/roast/workflow/session_manager.rb +82 -0
- data/lib/roast/workflow/state_repository.rb +21 -0
- data/lib/roast/workflow/workflow_executor.rb +99 -9
- data/lib/roast/workflow.rb +4 -0
- data/lib/roast.rb +2 -5
- data/roast.gemspec +1 -1
- data/schema/workflow.json +12 -0
- metadata +31 -6
- data/.rspec +0 -1
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/isolated_execution_state"
|
3
5
|
require "active_support/cache"
|
4
6
|
require "active_support/notifications"
|
5
7
|
require_relative "logger"
|
@@ -10,17 +12,79 @@ module Roast
|
|
10
12
|
# This module wraps around Raix::FunctionDispatch to provide caching for tool functions
|
11
13
|
module FunctionCachingInterceptor
|
12
14
|
def dispatch_tool_function(function_name, params)
|
13
|
-
|
14
|
-
return super(function_name, params) if configuration.blank?
|
15
|
+
start_time = Time.now
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
ActiveSupport::Notifications.instrument("roast.tool.execute", {
|
18
|
+
function_name: function_name,
|
19
|
+
params: params,
|
20
|
+
})
|
21
|
+
|
22
|
+
# Handle workflows with or without configuration
|
23
|
+
result = if !respond_to?(:configuration) || configuration.nil?
|
22
24
|
super(function_name, params)
|
25
|
+
else
|
26
|
+
function_config = if configuration.respond_to?(:function_config)
|
27
|
+
configuration.function_config(function_name)
|
28
|
+
else
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if caching is enabled - handle both formats:
|
33
|
+
# 1. cache: true (boolean format)
|
34
|
+
# 2. cache: { enabled: true } (hash format)
|
35
|
+
cache_enabled = if function_config.is_a?(Hash)
|
36
|
+
cache_config = function_config["cache"]
|
37
|
+
if cache_config.is_a?(Hash)
|
38
|
+
cache_config["enabled"]
|
39
|
+
else
|
40
|
+
# Direct boolean value
|
41
|
+
cache_config
|
42
|
+
end
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
if cache_enabled
|
48
|
+
# Call the original function and pass in the cache
|
49
|
+
super(function_name, params, cache: Roast::Tools::CACHE)
|
50
|
+
else
|
51
|
+
Roast::Helpers::Logger.debug("⚠️ Caching not enabled for #{function_name}")
|
52
|
+
super(function_name, params)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
execution_time = Time.now - start_time
|
57
|
+
|
58
|
+
# Determine if caching was enabled for metrics
|
59
|
+
cache_enabled = if defined?(function_config) && function_config.is_a?(Hash)
|
60
|
+
cache_config = function_config["cache"]
|
61
|
+
if cache_config.is_a?(Hash)
|
62
|
+
cache_config["enabled"]
|
63
|
+
else
|
64
|
+
# Direct boolean value
|
65
|
+
cache_config
|
66
|
+
end
|
67
|
+
else
|
68
|
+
false
|
23
69
|
end
|
70
|
+
|
71
|
+
ActiveSupport::Notifications.instrument("roast.tool.complete", {
|
72
|
+
function_name: function_name,
|
73
|
+
execution_time: execution_time,
|
74
|
+
cache_enabled: cache_enabled,
|
75
|
+
})
|
76
|
+
|
77
|
+
result
|
78
|
+
rescue => e
|
79
|
+
execution_time = Time.now - start_time
|
80
|
+
|
81
|
+
ActiveSupport::Notifications.instrument("roast.tool.error", {
|
82
|
+
function_name: function_name,
|
83
|
+
error: e.class.name,
|
84
|
+
message: e.message,
|
85
|
+
execution_time: execution_time,
|
86
|
+
})
|
87
|
+
raise
|
24
88
|
end
|
25
89
|
end
|
26
90
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Resources
|
9
|
+
# Resource implementation for API endpoints using Fetch API-style format
|
10
|
+
class ApiResource < BaseResource
|
11
|
+
def process
|
12
|
+
# For API resources, the target might be a JSON configuration or endpoint URL
|
13
|
+
target
|
14
|
+
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
return @config if defined?(@config)
|
18
|
+
|
19
|
+
@config = if target.is_a?(String) && target.match?(/^\s*{/)
|
20
|
+
begin
|
21
|
+
JSON.parse(target)
|
22
|
+
rescue JSON::ParserError
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def api_url
|
29
|
+
if config && config["url"]
|
30
|
+
config["url"]
|
31
|
+
else
|
32
|
+
# Assume direct URL
|
33
|
+
target
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def options
|
38
|
+
return {} unless config
|
39
|
+
|
40
|
+
config["options"] || {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def http_method
|
44
|
+
method_name = (options["method"] || "GET").upcase
|
45
|
+
case method_name
|
46
|
+
when "GET" then Net::HTTP::Get
|
47
|
+
when "POST" then Net::HTTP::Post
|
48
|
+
when "PUT" then Net::HTTP::Put
|
49
|
+
when "DELETE" then Net::HTTP::Delete
|
50
|
+
when "PATCH" then Net::HTTP::Patch
|
51
|
+
when "HEAD" then Net::HTTP::Head
|
52
|
+
else Net::HTTP::Get
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def exists?
|
57
|
+
return false unless target
|
58
|
+
|
59
|
+
uri = URI.parse(api_url)
|
60
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
61
|
+
http.use_ssl = (uri.scheme == "https")
|
62
|
+
|
63
|
+
# Use HEAD request to check existence
|
64
|
+
request = Net::HTTP::Head.new(uri.path.empty? ? "/" : uri.path)
|
65
|
+
|
66
|
+
# Add headers if present in options
|
67
|
+
if options["headers"].is_a?(Hash)
|
68
|
+
options["headers"].each do |key, value|
|
69
|
+
# Process any environment variables in header values
|
70
|
+
processed_value = process_env_vars(value.to_s)
|
71
|
+
request[key] = processed_value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Make the request
|
76
|
+
response = http.request(request)
|
77
|
+
|
78
|
+
# Consider 2xx and 3xx as success
|
79
|
+
response.code.to_i < 400
|
80
|
+
rescue StandardError => e
|
81
|
+
# Log the error but don't crash
|
82
|
+
Roast::Helpers::Logger.error("Error checking API existence: #{e.message}")
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def contents
|
87
|
+
return unless target
|
88
|
+
|
89
|
+
# If it's a configuration, return a prepared request object
|
90
|
+
if config
|
91
|
+
JSON.pretty_generate({
|
92
|
+
"url" => api_url,
|
93
|
+
"method" => (options["method"] || "GET").upcase,
|
94
|
+
"headers" => options["headers"] || {},
|
95
|
+
"body" => options["body"],
|
96
|
+
})
|
97
|
+
else
|
98
|
+
# Assume it's a direct API URL, do a simple GET
|
99
|
+
begin
|
100
|
+
uri = URI.parse(target)
|
101
|
+
Net::HTTP.get(uri)
|
102
|
+
rescue StandardError => e
|
103
|
+
# Log the error but don't crash
|
104
|
+
Roast::Helpers::Logger.error("Error fetching API contents: #{e.message}")
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def name
|
111
|
+
if target
|
112
|
+
if config && config["url"]
|
113
|
+
"API #{config["url"]} (#{(options["method"] || "GET").upcase})"
|
114
|
+
else
|
115
|
+
"API #{target}"
|
116
|
+
end
|
117
|
+
else
|
118
|
+
"Unnamed API"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def type
|
123
|
+
:api
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Replace environment variables in the format ${VAR_NAME}
|
129
|
+
def process_env_vars(text)
|
130
|
+
text.gsub(/\${([^}]+)}/) do |_match|
|
131
|
+
var_name = ::Regexp.last_match(1).strip
|
132
|
+
ENV.fetch(var_name, "${#{var_name}}")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Base class for all resource types
|
6
|
+
# Follows the Strategy pattern to handle different resource types in a polymorphic way
|
7
|
+
class BaseResource
|
8
|
+
attr_reader :target
|
9
|
+
|
10
|
+
# Initialize a resource with a target
|
11
|
+
# @param target [String] The target specified in the workflow, can be nil
|
12
|
+
def initialize(target)
|
13
|
+
@target = target
|
14
|
+
end
|
15
|
+
|
16
|
+
# Process the resource to prepare it for use
|
17
|
+
# @return [String] The processed target
|
18
|
+
def process
|
19
|
+
target
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if the resource exists
|
23
|
+
# @return [Boolean] true if the resource exists
|
24
|
+
def exists?
|
25
|
+
false # Override in subclasses
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the contents of the resource
|
29
|
+
# @return [String] The contents of the resource
|
30
|
+
def contents
|
31
|
+
nil # Override in subclasses
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a name for the resource to display in logs
|
35
|
+
# @return [String] A descriptive name for the resource
|
36
|
+
def name
|
37
|
+
target || "Unnamed Resource"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the type of this resource as a symbol
|
41
|
+
# @return [Symbol] The resource type
|
42
|
+
def type
|
43
|
+
:unknown
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Resource implementation for directories
|
6
|
+
class DirectoryResource < BaseResource
|
7
|
+
def process
|
8
|
+
if target.include?("*") || target.include?("?")
|
9
|
+
# If it's a glob pattern, return only directories
|
10
|
+
Dir.glob(target).select { |f| Dir.exist?(f) }.map { |d| File.expand_path(d) }.join("\n")
|
11
|
+
else
|
12
|
+
File.expand_path(target)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
Dir.exist?(target)
|
18
|
+
end
|
19
|
+
|
20
|
+
def contents
|
21
|
+
# Return a listing of files in the directory
|
22
|
+
if exists?
|
23
|
+
Dir.entries(target).reject { |f| f == "." || f == ".." }.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
if target
|
29
|
+
File.basename(target) + "/"
|
30
|
+
else
|
31
|
+
"Unnamed Directory"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def type
|
36
|
+
:directory
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Resource implementation for files
|
6
|
+
class FileResource < BaseResource
|
7
|
+
def process
|
8
|
+
# Handle glob patterns in the target
|
9
|
+
if target.include?("*") || target.include?("?")
|
10
|
+
Dir.glob(target).map { |f| File.expand_path(f) unless Dir.exist?(f) }.compact.join("\n")
|
11
|
+
else
|
12
|
+
File.expand_path(target)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
File.exist?(target) && !Dir.exist?(target)
|
18
|
+
end
|
19
|
+
|
20
|
+
def contents
|
21
|
+
File.read(target) if exists?
|
22
|
+
end
|
23
|
+
|
24
|
+
def name
|
25
|
+
File.basename(target) if target
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
:file
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Resource implementation for workflows with no target (targetless workflows)
|
6
|
+
class NoneResource < BaseResource
|
7
|
+
def process
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def exists?
|
12
|
+
# There's no target to check, so this is always true
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def contents
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
"Targetless Resource"
|
22
|
+
end
|
23
|
+
|
24
|
+
def type
|
25
|
+
:none
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Resources
|
8
|
+
# Resource implementation for URLs
|
9
|
+
class UrlResource < BaseResource
|
10
|
+
def process
|
11
|
+
# URLs don't need special processing, just return as is
|
12
|
+
target
|
13
|
+
end
|
14
|
+
|
15
|
+
def exists?
|
16
|
+
return false unless target
|
17
|
+
|
18
|
+
begin
|
19
|
+
uri = URI.parse(target)
|
20
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
21
|
+
http.use_ssl = (uri.scheme == "https")
|
22
|
+
|
23
|
+
# Just check the head to see if the resource exists
|
24
|
+
response = http.request_head(uri.path.empty? ? "/" : uri.path)
|
25
|
+
|
26
|
+
# Consider 2xx and 3xx as success
|
27
|
+
response.code.to_i < 400
|
28
|
+
rescue StandardError => e
|
29
|
+
# Log the error but don't crash
|
30
|
+
Roast::Helpers::Logger.error("Error checking URL existence: #{e.message}")
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def contents
|
36
|
+
return unless target
|
37
|
+
|
38
|
+
begin
|
39
|
+
uri = URI.parse(target)
|
40
|
+
Net::HTTP.get(uri)
|
41
|
+
rescue StandardError => e
|
42
|
+
# Log the error but don't crash
|
43
|
+
Roast::Helpers::Logger.error("Error fetching URL contents: #{e.message}")
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
if target
|
50
|
+
URI.parse(target).host
|
51
|
+
else
|
52
|
+
"Unnamed URL"
|
53
|
+
end
|
54
|
+
rescue URI::InvalidURIError
|
55
|
+
"Invalid URL"
|
56
|
+
end
|
57
|
+
|
58
|
+
def type
|
59
|
+
:url
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "resources/base_resource"
|
4
|
+
require_relative "resources/file_resource"
|
5
|
+
require_relative "resources/directory_resource"
|
6
|
+
require_relative "resources/url_resource"
|
7
|
+
require_relative "resources/api_resource"
|
8
|
+
require_relative "resources/none_resource"
|
9
|
+
require "uri"
|
10
|
+
|
11
|
+
module Roast
|
12
|
+
# The Resources module contains classes for handling different types of resources
|
13
|
+
# that workflows can operate on. Each resource type implements a common interface.
|
14
|
+
module Resources
|
15
|
+
extend self
|
16
|
+
|
17
|
+
# Create the appropriate resource based on the target
|
18
|
+
# @param target [String] The target specified in the workflow
|
19
|
+
# @return [BaseResource] A resource object of the appropriate type
|
20
|
+
def for(target)
|
21
|
+
type = detect_type(target)
|
22
|
+
|
23
|
+
case type
|
24
|
+
when :file
|
25
|
+
FileResource.new(target)
|
26
|
+
when :directory
|
27
|
+
DirectoryResource.new(target)
|
28
|
+
when :url
|
29
|
+
UrlResource.new(target)
|
30
|
+
when :api
|
31
|
+
ApiResource.new(target)
|
32
|
+
when :command
|
33
|
+
CommandResource.new(target)
|
34
|
+
when :none
|
35
|
+
NoneResource.new(target)
|
36
|
+
else
|
37
|
+
BaseResource.new(target) # Default to base resource
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Determines the resource type from the target
|
42
|
+
# @param target [String] The target specified in the workflow
|
43
|
+
# @return [Symbol] :file, :directory, :url, :api, or :none
|
44
|
+
def detect_type(target)
|
45
|
+
return :none if target.nil? || target.strip.empty?
|
46
|
+
|
47
|
+
# Check for command syntax $(...)
|
48
|
+
if target.match?(/^\$\(.*\)$/)
|
49
|
+
return :command
|
50
|
+
end
|
51
|
+
|
52
|
+
# Check for URLs
|
53
|
+
if target.start_with?("http://", "https://", "ftp://")
|
54
|
+
return :url
|
55
|
+
end
|
56
|
+
|
57
|
+
# Try to parse as URI to detect other URL schemes
|
58
|
+
begin
|
59
|
+
uri = URI.parse(target)
|
60
|
+
return :url if uri.scheme && uri.host
|
61
|
+
rescue URI::InvalidURIError
|
62
|
+
# Not a URL, continue with other checks
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check for directory
|
66
|
+
if Dir.exist?(target)
|
67
|
+
return :directory
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check for glob patterns (containing * or ?)
|
71
|
+
if target.include?("*") || target.include?("?")
|
72
|
+
matches = Dir.glob(target)
|
73
|
+
return :none if matches.empty?
|
74
|
+
# If the glob matches only directories, treat as directory type
|
75
|
+
return :directory if matches.all? { |path| Dir.exist?(path) }
|
76
|
+
|
77
|
+
# Otherwise treat as file type (could be mixed or all files)
|
78
|
+
return :file
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check for file
|
82
|
+
if File.exist?(target)
|
83
|
+
return :file
|
84
|
+
end
|
85
|
+
|
86
|
+
# Check for API configuration in Fetch API style format
|
87
|
+
begin
|
88
|
+
potential_config = JSON.parse(target)
|
89
|
+
if potential_config.is_a?(Hash) && potential_config.key?("url") && potential_config.key?("options")
|
90
|
+
return :api
|
91
|
+
end
|
92
|
+
rescue JSON::ParserError
|
93
|
+
# Not a JSON string, continue with other checks
|
94
|
+
end
|
95
|
+
|
96
|
+
# Default to file for anything else
|
97
|
+
:file
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "roast/helpers/logger"
|
4
|
+
require "open3"
|
5
|
+
require "tempfile"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
module Roast
|
9
|
+
module Tools
|
10
|
+
module CodingAgent
|
11
|
+
extend self
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def included(base)
|
15
|
+
base.class_eval do
|
16
|
+
function(
|
17
|
+
:coding_agent,
|
18
|
+
"AI-powered coding agent that runs Claude Code CLI with the given prompt",
|
19
|
+
prompt: { type: "string", description: "The prompt to send to Claude Code" },
|
20
|
+
) do |params|
|
21
|
+
Roast::Tools::CodingAgent.call(params[:prompt])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(prompt)
|
28
|
+
Roast::Helpers::Logger.info("🤖 Running CodingAgent\n")
|
29
|
+
run_claude_code(prompt)
|
30
|
+
rescue StandardError => e
|
31
|
+
"Error running CodingAgent: #{e.message}".tap do |error_message|
|
32
|
+
Roast::Helpers::Logger.error(error_message + "\n")
|
33
|
+
Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def run_claude_code(prompt)
|
40
|
+
Roast::Helpers::Logger.debug("🤖 Executing Claude Code CLI with prompt: #{prompt}\n")
|
41
|
+
|
42
|
+
# Create a temporary file with a unique name
|
43
|
+
timestamp = Time.now.to_i
|
44
|
+
random_id = SecureRandom.hex(8)
|
45
|
+
pid = Process.pid
|
46
|
+
temp_file = Tempfile.new(["claude_prompt_#{timestamp}_#{pid}_#{random_id}", ".txt"])
|
47
|
+
|
48
|
+
begin
|
49
|
+
# Write the prompt to the file
|
50
|
+
temp_file.write(prompt)
|
51
|
+
temp_file.close
|
52
|
+
|
53
|
+
# Run Claude Code CLI using the temp file as input
|
54
|
+
claude_code_command = ENV.fetch("CLAUDE_CODE_COMMAND", "claude -p")
|
55
|
+
stdout, stderr, status = Open3.capture3("cat #{temp_file.path} | #{claude_code_command}")
|
56
|
+
|
57
|
+
if status.success?
|
58
|
+
stdout
|
59
|
+
else
|
60
|
+
"Error running ClaudeCode: #{stderr}"
|
61
|
+
end
|
62
|
+
ensure
|
63
|
+
# Always clean up the temp file
|
64
|
+
temp_file.unlink
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/roast/tools.rb
CHANGED
data/lib/roast/version.rb
CHANGED