roast-ai 0.1.7 → 0.2.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.
Files changed (125) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yaml +1 -1
  3. data/CHANGELOG.md +49 -1
  4. data/CLAUDE.md +20 -0
  5. data/CLAUDE_NOTES.md +68 -0
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +9 -6
  8. data/README.md +159 -26
  9. data/bin/roast +27 -0
  10. data/docs/ITERATION_SYNTAX.md +147 -0
  11. data/examples/case_when/README.md +58 -0
  12. data/examples/case_when/detect_language/prompt.md +16 -0
  13. data/examples/case_when/workflow.yml +58 -0
  14. data/examples/conditional/README.md +161 -0
  15. data/examples/conditional/check_condition/prompt.md +1 -0
  16. data/examples/conditional/simple_workflow.yml +15 -0
  17. data/examples/conditional/workflow.yml +23 -0
  18. data/examples/direct_coerce_syntax/README.md +32 -0
  19. data/examples/direct_coerce_syntax/workflow.yml +36 -0
  20. data/examples/dot_notation/README.md +37 -0
  21. data/examples/dot_notation/workflow.yml +44 -0
  22. data/examples/exit_on_error/README.md +50 -0
  23. data/examples/exit_on_error/analyze_lint_output/prompt.md +9 -0
  24. data/examples/exit_on_error/apply_fixes/prompt.md +2 -0
  25. data/examples/exit_on_error/workflow.yml +19 -0
  26. data/examples/grading/workflow.yml +10 -4
  27. data/examples/iteration/IMPLEMENTATION.md +88 -0
  28. data/examples/iteration/README.md +68 -0
  29. data/examples/iteration/analyze_complexity/prompt.md +22 -0
  30. data/examples/iteration/generate_recommendations/prompt.md +21 -0
  31. data/examples/iteration/generate_report/prompt.md +129 -0
  32. data/examples/iteration/implement_fix/prompt.md +25 -0
  33. data/examples/iteration/prioritize_issues/prompt.md +24 -0
  34. data/examples/iteration/prompts/analyze_file.md +28 -0
  35. data/examples/iteration/prompts/generate_summary.md +24 -0
  36. data/examples/iteration/prompts/update_report.md +29 -0
  37. data/examples/iteration/prompts/write_report.md +22 -0
  38. data/examples/iteration/read_file/prompt.md +9 -0
  39. data/examples/iteration/select_next_issue/prompt.md +25 -0
  40. data/examples/iteration/simple_workflow.md +39 -0
  41. data/examples/iteration/simple_workflow.yml +58 -0
  42. data/examples/iteration/update_fix_count/prompt.md +26 -0
  43. data/examples/iteration/verify_fix/prompt.md +29 -0
  44. data/examples/iteration/workflow.yml +42 -0
  45. data/examples/json_handling/README.md +32 -0
  46. data/examples/json_handling/workflow.yml +52 -0
  47. data/examples/openrouter_example/workflow.yml +2 -2
  48. data/examples/smart_coercion_defaults/README.md +65 -0
  49. data/examples/smart_coercion_defaults/workflow.yml +44 -0
  50. data/examples/step_configuration/README.md +87 -0
  51. data/examples/step_configuration/workflow.yml +60 -0
  52. data/examples/workflow_generator/README.md +27 -0
  53. data/examples/workflow_generator/analyze_user_request/prompt.md +34 -0
  54. data/examples/workflow_generator/create_workflow_files/prompt.md +32 -0
  55. data/examples/workflow_generator/get_user_input/prompt.md +14 -0
  56. data/examples/workflow_generator/info_from_roast.rb +22 -0
  57. data/examples/workflow_generator/workflow.yml +35 -0
  58. data/lib/roast/errors.rb +9 -0
  59. data/lib/roast/factories/api_provider_factory.rb +61 -0
  60. data/lib/roast/helpers/function_caching_interceptor.rb +1 -1
  61. data/lib/roast/helpers/minitest_coverage_runner.rb +1 -1
  62. data/lib/roast/helpers/prompt_loader.rb +50 -1
  63. data/lib/roast/resources/base_resource.rb +7 -0
  64. data/lib/roast/resources.rb +6 -6
  65. data/lib/roast/tools/ask_user.rb +40 -0
  66. data/lib/roast/tools/cmd.rb +1 -1
  67. data/lib/roast/tools/search_file.rb +1 -1
  68. data/lib/roast/tools.rb +11 -1
  69. data/lib/roast/value_objects/api_token.rb +49 -0
  70. data/lib/roast/value_objects/step_name.rb +39 -0
  71. data/lib/roast/value_objects/workflow_path.rb +77 -0
  72. data/lib/roast/value_objects.rb +5 -0
  73. data/lib/roast/version.rb +1 -1
  74. data/lib/roast/workflow/api_configuration.rb +61 -0
  75. data/lib/roast/workflow/base_iteration_step.rb +184 -0
  76. data/lib/roast/workflow/base_step.rb +44 -27
  77. data/lib/roast/workflow/base_workflow.rb +76 -73
  78. data/lib/roast/workflow/case_executor.rb +49 -0
  79. data/lib/roast/workflow/case_step.rb +82 -0
  80. data/lib/roast/workflow/command_executor.rb +88 -0
  81. data/lib/roast/workflow/conditional_executor.rb +50 -0
  82. data/lib/roast/workflow/conditional_step.rb +59 -0
  83. data/lib/roast/workflow/configuration.rb +35 -158
  84. data/lib/roast/workflow/configuration_loader.rb +78 -0
  85. data/lib/roast/workflow/configuration_parser.rb +13 -248
  86. data/lib/roast/workflow/context_path_resolver.rb +43 -0
  87. data/lib/roast/workflow/dot_access_hash.rb +198 -0
  88. data/lib/roast/workflow/each_step.rb +86 -0
  89. data/lib/roast/workflow/error_handler.rb +97 -0
  90. data/lib/roast/workflow/expression_evaluator.rb +78 -0
  91. data/lib/roast/workflow/expression_utils.rb +36 -0
  92. data/lib/roast/workflow/file_state_repository.rb +3 -2
  93. data/lib/roast/workflow/interpolator.rb +34 -0
  94. data/lib/roast/workflow/iteration_executor.rb +103 -0
  95. data/lib/roast/workflow/llm_boolean_coercer.rb +55 -0
  96. data/lib/roast/workflow/output_handler.rb +35 -0
  97. data/lib/roast/workflow/output_manager.rb +77 -0
  98. data/lib/roast/workflow/parallel_executor.rb +49 -0
  99. data/lib/roast/workflow/prompt_step.rb +4 -1
  100. data/lib/roast/workflow/repeat_step.rb +75 -0
  101. data/lib/roast/workflow/replay_handler.rb +123 -0
  102. data/lib/roast/workflow/resource_resolver.rb +77 -0
  103. data/lib/roast/workflow/session_manager.rb +6 -2
  104. data/lib/roast/workflow/state_manager.rb +97 -0
  105. data/lib/roast/workflow/step_executor_coordinator.rb +221 -0
  106. data/lib/roast/workflow/step_executor_factory.rb +47 -0
  107. data/lib/roast/workflow/step_executor_registry.rb +79 -0
  108. data/lib/roast/workflow/step_executors/base_step_executor.rb +23 -0
  109. data/lib/roast/workflow/step_executors/hash_step_executor.rb +43 -0
  110. data/lib/roast/workflow/step_executors/parallel_step_executor.rb +54 -0
  111. data/lib/roast/workflow/step_executors/string_step_executor.rb +29 -0
  112. data/lib/roast/workflow/step_finder.rb +97 -0
  113. data/lib/roast/workflow/step_loader.rb +155 -0
  114. data/lib/roast/workflow/step_orchestrator.rb +45 -0
  115. data/lib/roast/workflow/step_runner.rb +23 -0
  116. data/lib/roast/workflow/step_type_resolver.rb +133 -0
  117. data/lib/roast/workflow/workflow_context.rb +60 -0
  118. data/lib/roast/workflow/workflow_executor.rb +90 -209
  119. data/lib/roast/workflow/workflow_initializer.rb +112 -0
  120. data/lib/roast/workflow/workflow_runner.rb +87 -0
  121. data/lib/roast/workflow.rb +3 -0
  122. data/lib/roast.rb +96 -3
  123. data/roast.gemspec +2 -1
  124. data/schema/workflow.json +112 -0
  125. metadata +112 -4
@@ -0,0 +1,35 @@
1
+ # Workflow Generator
2
+ #
3
+ # This workflow generates new Roast workflows based on user descriptions.
4
+ # It gets user input, analyzes the request, generates an appropriate workflow structure,
5
+ # and creates all necessary files in a new directory.
6
+
7
+ name: Workflow Generator
8
+ model: gpt-4o-mini
9
+
10
+ tools:
11
+ - Roast::Tools::WriteFile
12
+ - Roast::Tools::ReadFile
13
+ - Roast::Tools::Cmd
14
+ - Roast::Tools::AskUser
15
+
16
+ steps:
17
+ - get_user_input
18
+ - info_from_roast
19
+ - analyze_user_request
20
+ - create_workflow_files
21
+
22
+ # Step configurations
23
+ get_user_input:
24
+ print_response: false
25
+ json: true
26
+ auto_loop: false
27
+
28
+ analyze_user_request:
29
+ print_response: true
30
+
31
+ generate_workflow_structure:
32
+ print_response: true
33
+
34
+ create_workflow_files:
35
+ print_response: false
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ # Custom error for API resource not found (404) responses
5
+ class ResourceNotFoundError < StandardError; end
6
+
7
+ # Custom error for when API authentication fails
8
+ class AuthenticationError < StandardError; end
9
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module Factories
5
+ # Factory for determining and creating API provider configurations
6
+ class ApiProviderFactory
7
+ SUPPORTED_PROVIDERS = {
8
+ "openai" => :openai,
9
+ "openrouter" => :openrouter,
10
+ }.freeze
11
+
12
+ DEFAULT_PROVIDER = :openai
13
+
14
+ class << self
15
+ # Determines the API provider from configuration
16
+ # @param config [Hash] The configuration hash
17
+ # @return [Symbol] The API provider symbol (:openai or :openrouter)
18
+ def from_config(config)
19
+ return DEFAULT_PROVIDER unless config["api_provider"]
20
+
21
+ provider_name = config["api_provider"].to_s.downcase
22
+ provider = SUPPORTED_PROVIDERS[provider_name]
23
+
24
+ unless provider
25
+ Roast::Helpers::Logger.warn("Unknown API provider '#{provider_name}', defaulting to #{DEFAULT_PROVIDER}")
26
+ return DEFAULT_PROVIDER
27
+ end
28
+
29
+ provider
30
+ end
31
+
32
+ # Returns true if the provider is OpenRouter
33
+ # @param provider [Symbol] The provider symbol
34
+ # @return [Boolean]
35
+ def openrouter?(provider)
36
+ provider == :openrouter
37
+ end
38
+
39
+ # Returns true if the provider is OpenAI
40
+ # @param provider [Symbol] The provider symbol
41
+ # @return [Boolean]
42
+ def openai?(provider)
43
+ provider == :openai
44
+ end
45
+
46
+ # Returns the list of supported provider names
47
+ # @return [Array<String>]
48
+ def supported_provider_names
49
+ SUPPORTED_PROVIDERS.keys
50
+ end
51
+
52
+ # Validates a provider symbol
53
+ # @param provider [Symbol] The provider to validate
54
+ # @return [Boolean]
55
+ def valid_provider?(provider)
56
+ SUPPORTED_PROVIDERS.values.include?(provider)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,7 +4,7 @@ require "active_support"
4
4
  require "active_support/isolated_execution_state"
5
5
  require "active_support/cache"
6
6
  require "active_support/notifications"
7
- require_relative "logger"
7
+ require "roast/helpers/logger"
8
8
 
9
9
  module Roast
10
10
  module Helpers
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "coverage"
4
4
  require "minitest"
5
- require_relative "logger"
5
+ require "roast/helpers/logger"
6
6
 
7
7
  # Disable the built-in `at_exit` hook for Minitest before anything else
8
8
  module Minitest
@@ -89,11 +89,60 @@ module Roast
89
89
 
90
90
  def process_erb_if_needed(content)
91
91
  if content.include?("<%")
92
- ERB.new(content, trim_mode: "-").result(context.instance_eval { binding })
92
+ begin
93
+ ERB.new(content, trim_mode: "-").result(context.instance_eval { binding })
94
+ rescue TypeError => e
95
+ if e.message.include?("no implicit conversion of nil into String")
96
+ # Try to find which variable is causing the issue
97
+ variable_hint = detect_nil_variable(content)
98
+
99
+ error_message = <<~ERROR
100
+ This workflow requires a file or target to be specified.
101
+ #{variable_hint}
102
+
103
+ Usage: roast execute <workflow.yml> <file_or_pattern>
104
+
105
+ Examples:
106
+ roast execute #{context.respond_to?(:configuration) && context.configuration&.workflow_path || "workflow.yml"} test/my_test.rb
107
+ roast execute #{context.respond_to?(:configuration) && context.configuration&.workflow_path || "workflow.yml"} "test/**/*_test.rb"
108
+ ERROR
109
+ raise error_message
110
+ else
111
+ raise e
112
+ end
113
+ rescue NoMethodError => e
114
+ if e.message.include?("undefined method") && e.message.include?("for nil")
115
+ variable_hint = detect_nil_variable(content)
116
+
117
+ error_message = <<~ERROR
118
+ Error processing prompt template: #{e.message}
119
+ #{variable_hint}
120
+
121
+ This may indicate that the workflow requires a file or target to be specified.
122
+
123
+ Usage: roast execute <workflow.yml> <file_or_pattern>
124
+ ERROR
125
+ raise error_message
126
+ else
127
+ raise e
128
+ end
129
+ end
93
130
  else
94
131
  content
95
132
  end
96
133
  end
134
+
135
+ def detect_nil_variable(content)
136
+ if content.include?("workflow.file")
137
+ "The prompt template references 'workflow.file' but no file was provided."
138
+ elsif content.include?("<%= file %>")
139
+ "The prompt template references 'file' but no file was provided."
140
+ elsif content.match(/<%= .*?\.(\w+) %>/)
141
+ "The prompt template is trying to access a property that doesn't exist."
142
+ else
143
+ "The prompt template contains an ERB expression that references a nil value."
144
+ end
145
+ end
97
146
  end
98
147
  end
99
148
  end
@@ -19,6 +19,13 @@ module Roast
19
19
  target
20
20
  end
21
21
 
22
+ # Get the value of the resource (alias for target)
23
+ # Used for backward compatibility
24
+ # @return [String] The resource target value
25
+ def value
26
+ target
27
+ end
28
+
22
29
  # Check if the resource exists
23
30
  # @return [Boolean] true if the resource exists
24
31
  def exists?
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
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"
3
+ require "roast/resources/base_resource"
4
+ require "roast/resources/file_resource"
5
+ require "roast/resources/directory_resource"
6
+ require "roast/resources/url_resource"
7
+ require "roast/resources/api_resource"
8
+ require "roast/resources/none_resource"
9
9
  require "uri"
10
10
 
11
11
  module Roast
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/helpers/logger"
4
+
5
+ module Roast
6
+ module Tools
7
+ module AskUser
8
+ extend self
9
+
10
+ class << self
11
+ # Add this method to be included in other classes
12
+ def included(base)
13
+ base.class_eval do
14
+ function(
15
+ :ask_user,
16
+ "Ask the user for input with a specific prompt. Returns the user's response.",
17
+ prompt: { type: "string", description: "The prompt to show the user" },
18
+ ) do |params|
19
+ Roast::Tools::AskUser.call(params[:prompt])
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ def call(prompt)
26
+ Roast::Helpers::Logger.info("💬 Asking user: #{prompt}\n")
27
+
28
+ response = ::CLI::UI::Prompt.ask(prompt)
29
+
30
+ Roast::Helpers::Logger.info("User responded: #{response}\n")
31
+ response
32
+ rescue StandardError => e
33
+ "Error getting user input: #{e.message}".tap do |error_message|
34
+ Roast::Helpers::Logger.error(error_message + "\n")
35
+ Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -28,7 +28,7 @@ module Roast
28
28
  Roast::Helpers::Logger.info("🔧 Running command: #{command}\n")
29
29
 
30
30
  # Validate the command starts with one of the allowed prefixes
31
- allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev"]
31
+ allowed_prefixes = ["pwd", "find", "ls", "rake", "ruby", "dev", "mkdir"]
32
32
  command_prefix = command.split(" ").first
33
33
 
34
34
  err = "Error: Command not allowed. Only commands starting with #{allowed_prefixes.join(", ")} are permitted."
@@ -26,7 +26,7 @@ module Roast
26
26
  end
27
27
 
28
28
  def call(glob_pattern, path = ".")
29
- Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{path}'\n")
29
+ Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{File.expand_path(path)}'\n")
30
30
  search_for(glob_pattern, path).then do |results|
31
31
  return "No results found for #{glob_pattern} in #{path}" if results.empty?
32
32
  return read_contents(results.first) if results.size == 1
data/lib/roast/tools.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support/cache"
4
4
  require "English"
5
+ require "fileutils"
5
6
 
6
7
  require "roast/tools/grep"
7
8
  require "roast/tools/read_file"
@@ -10,12 +11,21 @@ require "roast/tools/write_file"
10
11
  require "roast/tools/update_files"
11
12
  require "roast/tools/cmd"
12
13
  require "roast/tools/coding_agent"
14
+ require "roast/tools/ask_user"
13
15
 
14
16
  module Roast
15
17
  module Tools
16
18
  extend self
17
19
 
18
- CACHE = ActiveSupport::Cache::FileStore.new(File.join(Dir.pwd, ".roast", "cache"))
20
+ # Initialize cache and ensure .gitignore exists
21
+ cache_dir = File.join(Dir.pwd, ".roast", "cache")
22
+ FileUtils.mkdir_p(cache_dir) unless File.directory?(cache_dir)
23
+
24
+ # Add .gitignore to cache directory
25
+ gitignore_path = File.join(cache_dir, ".gitignore")
26
+ File.write(gitignore_path, "*") unless File.exist?(gitignore_path)
27
+
28
+ CACHE = ActiveSupport::Cache::FileStore.new(cache_dir)
19
29
 
20
30
  def file_to_prompt(file)
21
31
  <<~PROMPT
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module ValueObjects
5
+ # Value object representing an API token with validation
6
+ class ApiToken
7
+ class InvalidTokenError < StandardError; end
8
+
9
+ attr_reader :value
10
+
11
+ def initialize(value)
12
+ @value = value&.to_s
13
+ validate!
14
+ freeze
15
+ end
16
+
17
+ def present?
18
+ !blank?
19
+ end
20
+
21
+ def blank?
22
+ @value.nil? || @value.strip.empty?
23
+ end
24
+
25
+ def to_s
26
+ @value
27
+ end
28
+
29
+ def ==(other)
30
+ return false unless other.is_a?(ApiToken)
31
+
32
+ value == other.value
33
+ end
34
+ alias_method :eql?, :==
35
+
36
+ def hash
37
+ [self.class, @value].hash
38
+ end
39
+
40
+ private
41
+
42
+ def validate!
43
+ return if @value.nil? # Allow nil tokens, just not empty strings
44
+
45
+ raise InvalidTokenError, "API token cannot be an empty string" if @value.strip.empty?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ module ValueObjects
5
+ # Value object representing a step name, which can be either a plain text prompt
6
+ # or a reference to a step file
7
+ class StepName
8
+ attr_reader :value
9
+
10
+ def initialize(name)
11
+ @value = name.to_s.strip
12
+ freeze
13
+ end
14
+
15
+ def plain_text?
16
+ @value.include?(" ")
17
+ end
18
+
19
+ def file_reference?
20
+ !plain_text?
21
+ end
22
+
23
+ def to_s
24
+ @value
25
+ end
26
+
27
+ def ==(other)
28
+ return false unless other.is_a?(StepName)
29
+
30
+ value == other.value
31
+ end
32
+ alias_method :eql?, :==
33
+
34
+ def hash
35
+ [self.class, @value].hash
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Roast
6
+ module ValueObjects
7
+ # Value object representing a workflow file path with validation and resolution
8
+ class WorkflowPath
9
+ class InvalidPathError < StandardError; end
10
+
11
+ attr_reader :value
12
+
13
+ def initialize(path)
14
+ @value = normalize_path(path)
15
+ @pathname = Pathname.new(@value)
16
+ validate!
17
+ freeze
18
+ end
19
+
20
+ def exist?
21
+ pathname.exist?
22
+ end
23
+
24
+ def absolute?
25
+ pathname.absolute?
26
+ end
27
+
28
+ def relative?
29
+ pathname.relative?
30
+ end
31
+
32
+ def dirname
33
+ pathname.dirname.to_s
34
+ end
35
+
36
+ def basename
37
+ pathname.basename.to_s
38
+ end
39
+
40
+ def to_s
41
+ @value
42
+ end
43
+
44
+ def to_path
45
+ @value
46
+ end
47
+
48
+ def ==(other)
49
+ return false unless other.is_a?(WorkflowPath)
50
+
51
+ value == other.value
52
+ end
53
+ alias_method :eql?, :==
54
+
55
+ def hash
56
+ [self.class, @value].hash
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :pathname
62
+
63
+ def normalize_path(path)
64
+ path.to_s.strip
65
+ end
66
+
67
+ def validate!
68
+ raise InvalidPathError, "Workflow path cannot be empty" if @value.empty?
69
+ raise InvalidPathError, "Workflow path must have .yml or .yaml extension" unless valid_extension?
70
+ end
71
+
72
+ def valid_extension?
73
+ @value.end_with?(".yml") || @value.end_with?(".yaml")
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/value_objects/api_token"
4
+ require "roast/value_objects/step_name"
5
+ require "roast/value_objects/workflow_path"
data/lib/roast/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Roast
4
- VERSION = "0.1.7"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roast/factories/api_provider_factory"
4
+ require "roast/workflow/resource_resolver"
5
+
6
+ module Roast
7
+ module Workflow
8
+ # Handles API-related configuration including tokens and providers
9
+ class ApiConfiguration
10
+ attr_reader :api_token, :api_provider
11
+
12
+ def initialize(config_hash)
13
+ @config_hash = config_hash
14
+ process_api_configuration
15
+ end
16
+
17
+ # Check if using OpenRouter
18
+ # @return [Boolean] true if using OpenRouter
19
+ def openrouter?
20
+ Roast::Factories::ApiProviderFactory.openrouter?(@api_provider)
21
+ end
22
+
23
+ # Check if using OpenAI
24
+ # @return [Boolean] true if using OpenAI
25
+ def openai?
26
+ Roast::Factories::ApiProviderFactory.openai?(@api_provider)
27
+ end
28
+
29
+ # Get the effective API token including environment variables
30
+ # @return [String, nil] The API token
31
+ def effective_token
32
+ @api_token || environment_token
33
+ end
34
+
35
+ private
36
+
37
+ def process_api_configuration
38
+ extract_api_token
39
+ extract_api_provider
40
+ end
41
+
42
+ def extract_api_token
43
+ if @config_hash["api_token"]
44
+ @api_token = ResourceResolver.process_shell_command(@config_hash["api_token"])
45
+ end
46
+ end
47
+
48
+ def extract_api_provider
49
+ @api_provider = Roast::Factories::ApiProviderFactory.from_config(@config_hash)
50
+ end
51
+
52
+ def environment_token
53
+ if openai?
54
+ ENV["OPENAI_API_KEY"]
55
+ elsif openrouter?
56
+ ENV["OPENROUTER_API_KEY"]
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end