roast-ai 0.1.5 → 0.1.7

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.
@@ -0,0 +1,12 @@
1
+ name: OpenRouter Example
2
+ api_provider: openrouter
3
+ api_token: $(echo $OPENROUTER_API_KEY)
4
+ model: anthropic/claude-3-haiku-20240307
5
+
6
+ tools:
7
+ - Roast::Tools::ReadFile
8
+ - Roast::Tools::WriteFile
9
+
10
+ steps:
11
+ - analyze_input
12
+ - generate_response
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roast
4
+ class Initializers
5
+ class << self
6
+ def config_root(starting_path = Dir.pwd, ending_path = File.dirname(Dir.home))
7
+ paths = []
8
+ candidate = starting_path
9
+ while candidate != ending_path
10
+ paths << File.join(candidate, ".roast")
11
+ candidate = File.dirname(candidate)
12
+ end
13
+
14
+ first_existing = paths.find { |path| Dir.exist?(path) }
15
+ first_existing || paths.first
16
+ end
17
+
18
+ def initializers_path
19
+ File.join(Roast::Initializers.config_root, "initializers")
20
+ end
21
+
22
+ def load_all
23
+ project_initializers = Roast::Initializers.initializers_path
24
+ return unless Dir.exist?(project_initializers)
25
+
26
+ $stderr.puts "Loading project initializers from #{project_initializers}"
27
+ pattern = File.join(project_initializers, "**/*.rb")
28
+ Dir.glob(pattern, sort: true).each do |file|
29
+ $stderr.puts "Loading initializer: #{file}"
30
+ require file
31
+ end
32
+ rescue => e
33
+ puts "ERROR: Error loading initializers: #{e.message}"
34
+ Roast::Helpers::Logger.error("Error loading initializers: #{e.message}")
35
+ # Don't fail the workflow if initializers can't be loaded
36
+ end
37
+ end
38
+ end
39
+ end
@@ -13,10 +13,11 @@ module Roast
13
13
  base.class_eval do
14
14
  function(
15
15
  :search_for_file,
16
- 'Search for a file in the project using `find . -type f -path "*#{@file_name}*"` in the project root',
17
- name: { type: "string", description: "filename with as much of the path as you can deduce" },
16
+ "Search for a file in the project using a glob pattern.",
17
+ glob_pattern: { type: "string", description: "A glob pattern to search for. Example: 'test/**/*_test.rb'" },
18
+ path: { type: "string", description: "path to search from" },
18
19
  ) do |params|
19
- Roast::Tools::SearchFile.call(params[:name]).tap do |result|
20
+ Roast::Tools::SearchFile.call(params[:glob_pattern], params[:path]).tap do |result|
20
21
  Roast::Helpers::Logger.debug(result) if ENV["DEBUG"]
21
22
  end
22
23
  end
@@ -24,27 +25,33 @@ module Roast
24
25
  end
25
26
  end
26
27
 
27
- def call(filename)
28
- Roast::Helpers::Logger.info("🔍 Searching for file: #{filename}\n")
29
- search_for(filename).then do |results|
30
- return "No results found for #{filename}" if results.empty?
31
- return Roast::Tools::ReadFile.call(results.first) if results.size == 1
28
+ def call(glob_pattern, path = ".")
29
+ Roast::Helpers::Logger.info("🔍 Searching for: '#{glob_pattern}' in '#{path}'\n")
30
+ search_for(glob_pattern, path).then do |results|
31
+ return "No results found for #{glob_pattern} in #{path}" if results.empty?
32
+ return read_contents(results.first) if results.size == 1
32
33
 
33
- results.inspect # purposely give the AI list of actual paths so that it can read without searching first
34
+ results.join("\n") # purposely give the AI list of actual paths so that it can read without searching first
34
35
  end
35
36
  rescue StandardError => e
36
- "Error searching for file: #{e.message}".tap do |error_message|
37
+ "Error searching for '#{glob_pattern}' in '#{path}': #{e.message}".tap do |error_message|
37
38
  Roast::Helpers::Logger.error(error_message + "\n")
38
39
  Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
39
40
  end
40
41
  end
41
42
 
42
- def search_for(filename)
43
- # Execute find command and get the output using -path to match against full paths
44
- result = %x(find . -type f -path "*#{filename}*").strip
43
+ def read_contents(path)
44
+ contents = File.read(path)
45
+ token_count = contents.size / 4
46
+ if token_count > 25_000
47
+ path
48
+ else
49
+ contents
50
+ end
51
+ end
45
52
 
46
- # Split by newlines and get the first result
47
- result.split("\n").map(&:strip).reject(&:empty?).map { |path| path.sub(%r{^\./}, "") }
53
+ def search_for(pattern, path)
54
+ Dir.glob(pattern, base: path)
48
55
  end
49
56
  end
50
57
  end
@@ -0,0 +1,413 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "roast/helpers/logger"
5
+ require "diff/lcs"
6
+
7
+ module Roast
8
+ module Tools
9
+ module UpdateFiles
10
+ extend self
11
+
12
+ class << self
13
+ # Add this method to be included in other classes
14
+ def included(base)
15
+ base.class_eval do
16
+ function(
17
+ :update_files,
18
+ "Apply a unified diff/patch to files in the workspace. Changes are applied atomically if possible.",
19
+ diff: {
20
+ type: "string",
21
+ description: "The unified diff/patch content to apply",
22
+ },
23
+ base_path: {
24
+ type: "string",
25
+ description: "Base path for relative file paths in the diff (default: current working directory)",
26
+ required: false,
27
+ },
28
+ restrict_path: {
29
+ type: "string",
30
+ description: "Optional path restriction to limit where files can be modified",
31
+ required: false,
32
+ },
33
+ create_files: {
34
+ type: "boolean",
35
+ description: "Whether to create new files if they don't exist (default: true)",
36
+ required: false,
37
+ },
38
+ ) do |params|
39
+ base_path = params[:base_path] || Dir.pwd
40
+ create_files = params.fetch(:create_files, true)
41
+ restrict_path = params[:restrict_path]
42
+
43
+ Roast::Tools::UpdateFiles.call(
44
+ params[:diff],
45
+ base_path,
46
+ restrict_path,
47
+ create_files,
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # Apply a unified diff to files
55
+ # @param diff [String] unified diff content
56
+ # @param base_path [String] base path for relative paths in the diff
57
+ # @param restrict_path [String, nil] optional path restriction
58
+ # @param create_files [Boolean] whether to create new files if they don't exist
59
+ # @return [String] result message
60
+ def call(diff, base_path = Dir.pwd, restrict_path = nil, create_files = true)
61
+ Roast::Helpers::Logger.info("🔄 Applying patch to files\n")
62
+
63
+ # Parse the unified diff to identify files and changes
64
+ file_changes = parse_unified_diff(diff)
65
+
66
+ if file_changes.empty?
67
+ return "Error: No valid file changes found in the provided diff"
68
+ end
69
+
70
+ # Validate changes
71
+ validation_result = validate_changes(file_changes, base_path, restrict_path, create_files)
72
+ return validation_result if validation_result.is_a?(String) && validation_result.start_with?("Error:")
73
+
74
+ # Apply changes atomically
75
+ apply_changes(file_changes, base_path, create_files)
76
+ rescue StandardError => e
77
+ "Error applying patch: #{e.message}".tap do |error_message|
78
+ Roast::Helpers::Logger.error(error_message + "\n")
79
+ Roast::Helpers::Logger.debug(e.backtrace.join("\n") + "\n") if ENV["DEBUG"]
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ # Parse a unified diff to extract file changes
86
+ # @param diff [String] unified diff content
87
+ # @return [Array<Hash>] array of file change objects
88
+ def parse_unified_diff(diff)
89
+ lines = diff.split("\n")
90
+ file_changes = []
91
+ current_files = { src: nil, dst: nil }
92
+ current_hunks = []
93
+
94
+ i = 0
95
+ while i < lines.length
96
+ line = lines[i]
97
+
98
+ # New file header (--- line followed by +++ line)
99
+ if line.start_with?("--- ") && i + 1 < lines.length && lines[i + 1].start_with?("+++ ")
100
+ # Save previous file if exists
101
+ if current_files[:src] && current_files[:dst] && !current_hunks.empty?
102
+ file_changes << {
103
+ src_path: current_files[:src],
104
+ dst_path: current_files[:dst],
105
+ hunks: current_hunks.dup,
106
+ }
107
+ end
108
+
109
+ # Extract new file paths
110
+ current_files = {
111
+ src: extract_file_path(line),
112
+ dst: extract_file_path(lines[i + 1]),
113
+ }
114
+ current_hunks = []
115
+ i += 2
116
+ next
117
+ end
118
+
119
+ # Hunk header
120
+ if line.match(/^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/)
121
+ current_hunk = { header: line, changes: [] }
122
+
123
+ # Parse the header
124
+ header_match = line.match(/@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/)
125
+ if header_match
126
+ current_hunk[:src_start] = header_match[1].to_i
127
+ current_hunk[:src_count] = header_match[2] ? header_match[2].to_i : 1 # rubocop:disable Metrics/BlockNesting
128
+ current_hunk[:dst_start] = header_match[3].to_i
129
+ current_hunk[:dst_count] = header_match[4] ? header_match[4].to_i : 1 # rubocop:disable Metrics/BlockNesting
130
+ end
131
+
132
+ current_hunks << current_hunk
133
+ i += 1
134
+ next
135
+ end
136
+
137
+ # Capture content lines for the current hunk
138
+ if !current_hunks.empty? && line.start_with?("+", "-", " ")
139
+ current_hunks.last[:changes] << line
140
+ end
141
+
142
+ i += 1
143
+ end
144
+
145
+ # Add the last file
146
+ if current_files[:src] && current_files[:dst] && !current_hunks.empty?
147
+ file_changes << {
148
+ src_path: current_files[:src],
149
+ dst_path: current_files[:dst],
150
+ hunks: current_hunks.dup,
151
+ }
152
+ end
153
+
154
+ file_changes
155
+ end
156
+
157
+ # Extract file path from a diff header line
158
+ # @param line [String] diff header line (--- or +++ line)
159
+ # @return [String] file path
160
+ def extract_file_path(line)
161
+ # Handle special cases
162
+ return "/dev/null" if line.include?("/dev/null")
163
+
164
+ # Remove prefixes
165
+ path = line.sub(%r{^(\+\+\+|\-\-\-) (a|b)/}, "")
166
+ # Handle files without 'a/' or 'b/' prefix
167
+ path = line.sub(/^(\+\+\+|\-\-\-) /, "") if path == line
168
+ # Remove timestamps if present
169
+ path = path.sub(/\t.*$/, "")
170
+ path
171
+ end
172
+
173
+ # Validate changes before applying them
174
+ # @param file_changes [Array<Hash>] array of file change objects
175
+ # @param base_path [String] base path for relative paths
176
+ # @param restrict_path [String, nil] optional path restriction
177
+ # @param create_files [Boolean] whether to create new files if they don't exist
178
+ # @return [Boolean, String] true if valid, error message if invalid
179
+ def validate_changes(file_changes, base_path, restrict_path, create_files)
180
+ # Validate each file in the changes
181
+ file_changes.each do |file_change|
182
+ # For destination files (they will be written to)
183
+ if file_change[:dst_path] && file_change[:dst_path] != "/dev/null"
184
+ absolute_path = File.expand_path(file_change[:dst_path], base_path)
185
+
186
+ # Check path restriction
187
+ if restrict_path && !absolute_path.start_with?(restrict_path)
188
+ return "Error: Path #{file_change[:dst_path]} must start with '#{restrict_path}' to use the update_files tool"
189
+ end
190
+
191
+ # Check if file exists
192
+ if !File.exist?(absolute_path) && !create_files
193
+ return "Error: File #{file_change[:dst_path]} does not exist and create_files is false"
194
+ end
195
+
196
+ # Check if file is readable and writable if it exists
197
+ if File.exist?(absolute_path)
198
+ unless File.readable?(absolute_path)
199
+ return "Error: File #{file_change[:dst_path]} is not readable"
200
+ end
201
+
202
+ unless File.writable?(absolute_path)
203
+ return "Error: File #{file_change[:dst_path]} is not writable"
204
+ end
205
+ end
206
+ end
207
+
208
+ # For source files (they will be read from)
209
+ next unless file_change[:src_path] && file_change[:src_path] != "/dev/null" && file_change[:src_path] != file_change[:dst_path]
210
+
211
+ absolute_path = File.expand_path(file_change[:src_path], base_path)
212
+
213
+ # Source file must exist unless it's a new file
214
+ if !File.exist?(absolute_path) && file_change[:src_path] != "/dev/null"
215
+ # Special case for new files (src: /dev/null)
216
+ if file_change[:src_path] != "/dev/null"
217
+ return "Error: Source file #{file_change[:src_path]} does not exist"
218
+ end
219
+ end
220
+
221
+ # Check if file is readable if it exists
222
+ next unless File.exist?(absolute_path)
223
+ unless File.readable?(absolute_path)
224
+ return "Error: Source file #{file_change[:src_path]} is not readable"
225
+ end
226
+ end
227
+
228
+ true
229
+ end
230
+
231
+ # Apply changes to files
232
+ # @param file_changes [Array<Hash>] array of file change objects
233
+ # @param base_path [String] base path for relative paths
234
+ # @param create_files [Boolean] whether to create new files if they don't exist
235
+ # @return [String] result message
236
+ def apply_changes(file_changes, base_path, create_files)
237
+ # Create a temporary backup of all files to be modified
238
+ backup_files = {}
239
+ modified_files = []
240
+
241
+ # Step 1: Create backups
242
+ file_changes.each do |file_change|
243
+ next unless file_change[:dst_path] && file_change[:dst_path] != "/dev/null"
244
+
245
+ absolute_path = File.expand_path(file_change[:dst_path], base_path)
246
+
247
+ if File.exist?(absolute_path)
248
+ backup_files[absolute_path] = File.read(absolute_path)
249
+ end
250
+ end
251
+
252
+ # Step 2: Try to apply all changes
253
+ begin
254
+ file_changes.each do |file_change|
255
+ next unless file_change[:dst_path]
256
+
257
+ # Special case for file deletion
258
+ if file_change[:dst_path] == "/dev/null" && file_change[:src_path] != "/dev/null"
259
+ absolute_src_path = File.expand_path(file_change[:src_path], base_path)
260
+ if File.exist?(absolute_src_path)
261
+ File.delete(absolute_src_path)
262
+ modified_files << file_change[:src_path]
263
+ end
264
+ next
265
+ end
266
+
267
+ # Skip if both src and dst are /dev/null (shouldn't happen but just in case)
268
+ next if file_change[:dst_path] == "/dev/null" && file_change[:src_path] == "/dev/null"
269
+
270
+ absolute_dst_path = File.expand_path(file_change[:dst_path], base_path)
271
+
272
+ # Special case for new files
273
+ if file_change[:src_path] == "/dev/null"
274
+ # For new files, ensure directory exists
275
+ dir = File.dirname(absolute_dst_path)
276
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
277
+
278
+ # Create the file with the added content
279
+ content = []
280
+ file_change[:hunks].each do |hunk|
281
+ hunk[:changes].each do |line|
282
+ content << line[1..-1] if line.start_with?("+")
283
+ end
284
+ end
285
+
286
+ # Write the content
287
+ File.write(absolute_dst_path, content.join("\n") + (content.empty? ? "" : "\n"))
288
+ modified_files << file_change[:dst_path]
289
+ next
290
+ end
291
+
292
+ # Normal case: Modify existing file
293
+ content = ""
294
+ if File.exist?(absolute_dst_path)
295
+ content = File.read(absolute_dst_path)
296
+ else
297
+ # For new files that aren't from /dev/null, ensure directory exists
298
+ dir = File.dirname(absolute_dst_path)
299
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
300
+ end
301
+ content_lines = content.split("\n")
302
+
303
+ # Apply each hunk to the file
304
+ file_change[:hunks].each do |hunk|
305
+ # Apply the changes to the content
306
+ new_content_lines = apply_hunk(content_lines, hunk)
307
+
308
+ # Check if the hunk was applied successfully
309
+ if new_content_lines
310
+ content_lines = new_content_lines
311
+ else
312
+ raise "Hunk could not be applied cleanly: #{hunk[:header]}"
313
+ end
314
+ end
315
+
316
+ # Write the updated content
317
+ File.write(absolute_dst_path, content_lines.join("\n") + (content_lines.empty? ? "" : "\n"))
318
+ modified_files << file_change[:dst_path]
319
+ end
320
+
321
+ "Successfully applied patch to #{modified_files.size} file(s): #{modified_files.join(", ")}"
322
+ rescue StandardError => e
323
+ # Restore backups if any change fails
324
+ backup_files.each do |path, content|
325
+ File.write(path, content) if File.exist?(path)
326
+ end
327
+
328
+ "Error applying patch: #{e.message}"
329
+ end
330
+ end
331
+
332
+ # Apply a single hunk to file content
333
+ # @param content_lines [Array<String>] lines of file content
334
+ # @param hunk [Hash] hunk information
335
+ # @return [Array<String>, nil] updated content lines or nil if cannot apply
336
+ def apply_hunk(content_lines, hunk)
337
+ # For completely new files with no content
338
+ if content_lines.empty? && hunk[:src_start] == 1 && hunk[:src_count] == 0
339
+ # Just extract the added lines
340
+ return hunk[:changes].select { |line| line.start_with?("+") }.map { |line| line[1..-1] }
341
+ end
342
+
343
+ # For complete file replacement
344
+ if !content_lines.empty? &&
345
+ hunk[:src_start] == 1 &&
346
+ hunk[:changes].count { |line| line.start_with?("-") } >= content_lines.size
347
+ # Get only the added lines for the new content
348
+ return hunk[:changes].select { |line| line.start_with?("+") }.map { |line| line[1..-1] }
349
+ end
350
+
351
+ # Standard case with context matching
352
+ result = content_lines.dup
353
+ src_line = hunk[:src_start] - 1 # 0-based index
354
+ dst_line = hunk[:dst_start] - 1 # 0-based index
355
+
356
+ # Process each change line
357
+ hunk[:changes].each do |line|
358
+ if line.start_with?(" ") # Context line
359
+ # Verify context matches
360
+ if src_line >= result.size || result[src_line] != line[1..-1]
361
+ # Try to find the context nearby (fuzzy matching)
362
+ found = false
363
+ (-3..3).each do |offset|
364
+ check_pos = src_line + offset
365
+ next if check_pos < 0 || check_pos >= result.size
366
+
367
+ next unless result[check_pos] == line[1..-1]
368
+
369
+ src_line = check_pos
370
+ dst_line = check_pos
371
+ found = true
372
+ break
373
+ end
374
+
375
+ return nil unless found # Context doesn't match, cannot apply hunk
376
+ end
377
+
378
+ src_line += 1
379
+ dst_line += 1
380
+ elsif line.start_with?("-") # Removal
381
+ # Verify line exists and matches
382
+ if src_line >= result.size || result[src_line] != line[1..-1]
383
+ # Try to find the line nearby (fuzzy matching)
384
+ found = false
385
+ (-3..3).each do |offset|
386
+ check_pos = src_line + offset
387
+ next if check_pos < 0 || check_pos >= result.size
388
+
389
+ next unless result[check_pos] == line[1..-1]
390
+
391
+ src_line = check_pos
392
+ dst_line = check_pos
393
+ found = true
394
+ break
395
+ end
396
+
397
+ return nil unless found # Line to remove doesn't match, cannot apply hunk
398
+ end
399
+
400
+ # Remove the line
401
+ result.delete_at(src_line)
402
+ elsif line.start_with?("+") # Addition
403
+ # Insert the new line
404
+ result.insert(dst_line, line[1..-1])
405
+ dst_line += 1
406
+ end
407
+ end
408
+
409
+ result
410
+ end
411
+ end
412
+ end
413
+ end
data/lib/roast/tools.rb CHANGED
@@ -7,6 +7,7 @@ require "roast/tools/grep"
7
7
  require "roast/tools/read_file"
8
8
  require "roast/tools/search_file"
9
9
  require "roast/tools/write_file"
10
+ require "roast/tools/update_files"
10
11
  require "roast/tools/cmd"
11
12
  require "roast/tools/coding_agent"
12
13
 
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.5"
4
+ VERSION = "0.1.7"
5
5
  end
@@ -8,7 +8,7 @@ module Roast
8
8
  # Encapsulates workflow configuration data and provides structured access
9
9
  # to the configuration settings
10
10
  class Configuration
11
- attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :model, :resource
11
+ attr_reader :config_hash, :workflow_path, :name, :steps, :tools, :function_configs, :api_token, :api_provider, :model, :resource
12
12
  attr_accessor :target
13
13
 
14
14
  def initialize(workflow_path, options = {})
@@ -45,6 +45,9 @@ module Roast
45
45
  @api_token = process_shell_command(@config_hash["api_token"])
46
46
  end
47
47
 
48
+ # Determine API provider (defaults to OpenAI if not specified)
49
+ @api_provider = determine_api_provider
50
+
48
51
  # Extract default model if provided
49
52
  @model = @config_hash["model"]
50
53
  end
@@ -138,8 +141,32 @@ module Roast
138
141
  @function_configs[function_name.to_s] || {}
139
142
  end
140
143
 
144
+ def openrouter?
145
+ @api_provider == :openrouter
146
+ end
147
+
148
+ def openai?
149
+ @api_provider == :openai
150
+ end
151
+
141
152
  private
142
153
 
154
+ def determine_api_provider
155
+ return :openai unless @config_hash["api_provider"]
156
+
157
+ provider = @config_hash["api_provider"].to_s.downcase
158
+
159
+ case provider
160
+ when "openai"
161
+ :openai
162
+ when "openrouter"
163
+ :openrouter
164
+ else
165
+ Roast::Helpers::Logger.warn("Unknown API provider '#{provider}', defaulting to OpenAI")
166
+ :openai
167
+ end
168
+ end
169
+
143
170
  def process_shell_command(command)
144
171
  # If it's a bash command with the $(command) syntax
145
172
  if command =~ /^\$\((.*)\)$/
@@ -6,6 +6,7 @@ require_relative "../helpers/function_caching_interceptor"
6
6
  require "active_support"
7
7
  require "active_support/isolated_execution_state"
8
8
  require "active_support/notifications"
9
+ require "raix"
9
10
 
10
11
  module Roast
11
12
  module Workflow
@@ -98,35 +99,23 @@ module Roast
98
99
  end
99
100
 
100
101
  def load_roast_initializers
101
- # Project-specific initializers
102
- project_initializers = File.join(Dir.pwd, ".roast", "initializers")
103
-
104
- if Dir.exist?(project_initializers)
105
- $stderr.puts "Loading project initializers from #{project_initializers}"
106
- Dir.glob(File.join(project_initializers, "**/*.rb")).sort.each do |file|
107
- $stderr.puts "Loading initializer: #{file}"
108
- require file
109
- end
110
- end
111
- rescue => e
112
- Roast::Helpers::Logger.error("Error loading initializers: #{e.message}")
113
- # Don't fail the workflow if initializers can't be loaded
102
+ Roast::Initializers.load_all
114
103
  end
115
104
 
116
105
  def configure_api_client
117
106
  return unless configuration.api_token
118
107
 
119
108
  begin
120
- require "raix"
121
-
122
- # Configure OpenAI client with the token
123
- $stderr.puts "Configuring API client with token from workflow"
109
+ case configuration.api_provider
110
+ when :openrouter
111
+ $stderr.puts "Configuring OpenRouter client with token from workflow"
112
+ require "open_router"
124
113
 
125
- # Initialize the OpenAI client if it doesn't exist
126
- if defined?(Raix.configuration.openai_client)
127
- # Create a new client with the token
128
- Raix.configuration.openai_client = OpenAI::Client.new(access_token: configuration.api_token)
114
+ Raix.configure do |config|
115
+ config.openrouter_client = OpenRouter::Client.new(api_key: configuration.api_token)
116
+ end
129
117
  else
118
+ $stderr.puts "Configuring OpenAI client with token from workflow"
130
119
  require "openai"
131
120
 
132
121
  Raix.configure do |config|
data/lib/roast.rb CHANGED
@@ -7,6 +7,7 @@ require "roast/tools"
7
7
  require "roast/helpers"
8
8
  require "roast/resources"
9
9
  require "roast/workflow"
10
+ require "roast/initializers"
10
11
 
11
12
  module Roast
12
13
  ROOT = File.expand_path("../..", __FILE__)
@@ -28,6 +29,11 @@ module Roast
28
29
  Roast::Workflow::ConfigurationParser.new(expanded_workflow_path, files, options.transform_keys(&:to_sym)).begin!
29
30
  end
30
31
 
32
+ desc "version", "Display the current version of Roast"
33
+ def version
34
+ puts "Roast version #{Roast::VERSION}"
35
+ end
36
+
31
37
  class << self
32
38
  def exit_on_failure?
33
39
  true
data/roast.gemspec CHANGED
@@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
37
37
  spec.require_paths = ["lib"]
38
38
 
39
39
  spec.add_dependency("activesupport", "~> 8.0")
40
+ spec.add_dependency("diff-lcs", "~> 1.5")
40
41
  spec.add_dependency("faraday-retry")
41
42
  spec.add_dependency("json-schema")
42
43
  spec.add_dependency("raix", "~> 0.8.4")