aider-ruby 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +542 -0
- data/bin/aider-ruby +320 -0
- data/lib/aider_ruby/client.rb +571 -0
- data/lib/aider_ruby/config.rb +371 -0
- data/lib/aider_ruby/error_handling.rb +57 -0
- data/lib/aider_ruby/models.rb +204 -0
- data/lib/aider_ruby/task_executor.rb +301 -0
- data/lib/aider_ruby/validation.rb +103 -0
- data/lib/aider_ruby/version.rb +3 -0
- data/lib/aider_ruby.rb +30 -0
- metadata +154 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
module AiderRuby
|
|
5
|
+
module Client
|
|
6
|
+
# Module for model configuration methods
|
|
7
|
+
module ModelConfiguration
|
|
8
|
+
def model(model_name)
|
|
9
|
+
@config.model = model_name
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def openai_api_key(key)
|
|
14
|
+
@config.openai_api_key = key
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def anthropic_api_key(key)
|
|
19
|
+
@config.anthropic_api_key = key
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reasoning_effort(effort)
|
|
24
|
+
@config.reasoning_effort = effort
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def thinking_tokens(tokens)
|
|
29
|
+
@config.thinking_tokens = tokens
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def use_temperature(enabled = true)
|
|
34
|
+
@config.use_temperature = enabled
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def use_system_prompt(enabled = true)
|
|
39
|
+
@config.use_system_prompt = enabled
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def use_repo_map(enabled = true)
|
|
44
|
+
@config.use_repo_map = enabled
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def extra_params(params)
|
|
49
|
+
@config.extra_params = params
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def model_settings_file(file_path)
|
|
54
|
+
@config.model_settings_file = file_path
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def model_metadata_file(file_path)
|
|
59
|
+
@config.model_metadata_file = file_path
|
|
60
|
+
self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def add_alias(alias_name, model_name)
|
|
64
|
+
@config.alias_settings ||= []
|
|
65
|
+
@config.alias_settings << { alias: alias_name, model: model_name }
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def reasoning_tag(tag)
|
|
70
|
+
@config.reasoning_tag = tag
|
|
71
|
+
self
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def weak_model_name(model_name)
|
|
75
|
+
@config.weak_model_name = model_name
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def editor_model_name(model_name)
|
|
80
|
+
@config.editor_model_name = model_name
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Module for output configuration methods
|
|
86
|
+
module OutputConfiguration
|
|
87
|
+
def dark_mode(enabled = true)
|
|
88
|
+
@config.dark_mode = enabled
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def light_mode(enabled = true)
|
|
93
|
+
@config.light_mode = enabled
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pretty(enabled = true)
|
|
98
|
+
@config.pretty = enabled
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def stream(enabled = true)
|
|
103
|
+
@config.stream = enabled
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Module for Git configuration methods
|
|
109
|
+
module GitConfiguration
|
|
110
|
+
def git(enabled = true)
|
|
111
|
+
@config.git = enabled
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def auto_commits(enabled = true)
|
|
116
|
+
@config.auto_commits = enabled
|
|
117
|
+
self
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Module for linting and testing configuration methods
|
|
122
|
+
module LintTestConfiguration
|
|
123
|
+
def lint(enabled = true)
|
|
124
|
+
@config.lint = enabled
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def auto_lint(enabled = true)
|
|
129
|
+
@config.auto_lint = enabled
|
|
130
|
+
self
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def test(enabled = true)
|
|
134
|
+
@config.test = enabled
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def auto_test(enabled = true)
|
|
139
|
+
@config.auto_test = enabled
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Module for general configuration methods
|
|
145
|
+
module GeneralConfiguration
|
|
146
|
+
def disable_playwright(enabled = true)
|
|
147
|
+
@config.disable_playwright = enabled
|
|
148
|
+
self
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def vim(enabled = true)
|
|
152
|
+
@config.vim = enabled
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def chat_language(language)
|
|
157
|
+
@config.chat_language = language
|
|
158
|
+
self
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def commit_language(language)
|
|
162
|
+
@config.commit_language = language
|
|
163
|
+
self
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def yes_always(enabled = true)
|
|
167
|
+
@config.yes_always = enabled
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def verbose(enabled = true)
|
|
172
|
+
@config.verbose = enabled
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def encoding(encoding)
|
|
177
|
+
@config.encoding = encoding
|
|
178
|
+
self
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def line_endings(endings)
|
|
182
|
+
@config.line_endings = endings
|
|
183
|
+
self
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def suggest_shell_commands(enabled = true)
|
|
187
|
+
@config.suggest_shell_commands = enabled
|
|
188
|
+
self
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def fancy_input(enabled = true)
|
|
192
|
+
@config.fancy_input = enabled
|
|
193
|
+
self
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def multiline(enabled = true)
|
|
197
|
+
@config.multiline = enabled
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def notifications(enabled = true)
|
|
202
|
+
@config.notifications = enabled
|
|
203
|
+
self
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def notifications_command(command)
|
|
207
|
+
@config.notifications_command = command
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def detect_urls(enabled = true)
|
|
212
|
+
@config.detect_urls = enabled
|
|
213
|
+
self
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def editor(editor)
|
|
217
|
+
@config.editor = editor
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def shell_completions(enabled = true)
|
|
222
|
+
@config.shell_completions = enabled
|
|
223
|
+
self
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Module for convention and edit format methods
|
|
228
|
+
module ConventionConfiguration
|
|
229
|
+
# Add multiple conventions files
|
|
230
|
+
def conventions_files(file_paths, validate: true)
|
|
231
|
+
file_paths = Array(file_paths)
|
|
232
|
+
|
|
233
|
+
if validate
|
|
234
|
+
file_paths.each do |path|
|
|
235
|
+
raise AiderRuby::ErrorHandling::FileError, "Conventions file not found: #{path}" unless File.exist?(path)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Store multiple conventions files
|
|
240
|
+
@config.conventions_files ||= []
|
|
241
|
+
@config.conventions_files.concat(file_paths)
|
|
242
|
+
self
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Enhanced read files method with validation and filtering
|
|
246
|
+
def add_read_files(files, validate: true, extensions: nil, exclude_patterns: [])
|
|
247
|
+
files = Array(files)
|
|
248
|
+
|
|
249
|
+
# Filter files by extensions if specified
|
|
250
|
+
if extensions
|
|
251
|
+
files = files.select do |file|
|
|
252
|
+
file_ext = File.extname(file)
|
|
253
|
+
extensions.include?(file_ext)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Exclude files matching patterns
|
|
258
|
+
files = files.reject do |file|
|
|
259
|
+
exclude_patterns.any? do |pattern|
|
|
260
|
+
case pattern
|
|
261
|
+
when String
|
|
262
|
+
file.include?(pattern)
|
|
263
|
+
when Regexp
|
|
264
|
+
file.match?(pattern)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Validate files exist if requested
|
|
270
|
+
if validate
|
|
271
|
+
files.each do |file|
|
|
272
|
+
raise AiderRuby::ErrorHandling::FileError, "Read file not found: #{file}" unless File.exist?(file)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
@config.read_files ||= []
|
|
277
|
+
@config.read_files.concat(files)
|
|
278
|
+
self
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Add read files from folder with filtering
|
|
282
|
+
def add_read_files_from_folder(folder_path, extensions: nil, exclude_patterns: [], validate: true)
|
|
283
|
+
require 'find'
|
|
284
|
+
|
|
285
|
+
files_in_folder = []
|
|
286
|
+
|
|
287
|
+
Find.find(folder_path) do |path|
|
|
288
|
+
next if File.directory?(path)
|
|
289
|
+
|
|
290
|
+
# Skip if file doesn't match extension filter
|
|
291
|
+
if extensions
|
|
292
|
+
file_ext = File.extname(path)
|
|
293
|
+
next unless extensions.include?(file_ext)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Skip if file matches exclude patterns
|
|
297
|
+
skip_file = exclude_patterns.any? do |pattern|
|
|
298
|
+
case pattern
|
|
299
|
+
when String
|
|
300
|
+
path.include?(pattern)
|
|
301
|
+
when Regexp
|
|
302
|
+
path.match?(pattern)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
next if skip_file
|
|
306
|
+
|
|
307
|
+
files_in_folder << path
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
add_read_files(files_in_folder, validate: validate)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Clear all read files
|
|
314
|
+
def clear_read_files
|
|
315
|
+
@config.read_files = []
|
|
316
|
+
self
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Get list of read files
|
|
320
|
+
def read_files_list
|
|
321
|
+
@config.read_files || []
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def edit_format_whole(enabled = true)
|
|
325
|
+
@config.edit_format_whole = enabled
|
|
326
|
+
self
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def edit_format_diff(enabled = true)
|
|
330
|
+
@config.edit_format_diff = enabled
|
|
331
|
+
self
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def edit_format_diff_fenced(enabled = true)
|
|
335
|
+
@config.edit_format_diff_fenced = enabled
|
|
336
|
+
self
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def editor_edit_format_whole(enabled = true)
|
|
340
|
+
@config.editor_edit_format_whole = enabled
|
|
341
|
+
self
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def editor_edit_format_diff(enabled = true)
|
|
345
|
+
@config.editor_edit_format_diff = enabled
|
|
346
|
+
self
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def editor_edit_format_diff_fenced(enabled = true)
|
|
350
|
+
@config.editor_edit_format_diff_fenced = enabled
|
|
351
|
+
self
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Module for execution methods
|
|
356
|
+
module ExecutionMethods
|
|
357
|
+
def execute(message, options = {})
|
|
358
|
+
args = build_command_args(options)
|
|
359
|
+
args << '--message' << message
|
|
360
|
+
|
|
361
|
+
execute_command(args)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def interactive(options = {})
|
|
365
|
+
args = build_command_args(options)
|
|
366
|
+
|
|
367
|
+
execute_command(args, interactive: true)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def execute_from_file(message_file, options = {})
|
|
371
|
+
args = build_command_args(options)
|
|
372
|
+
args << '--message-file' << message_file
|
|
373
|
+
|
|
374
|
+
execute_command(args)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def apply_changes(file_path, options = {})
|
|
378
|
+
args = build_command_args(options)
|
|
379
|
+
args << '--apply' << file_path
|
|
380
|
+
|
|
381
|
+
execute_command(args)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def show_repo_map(options = {})
|
|
385
|
+
args = build_command_args(options)
|
|
386
|
+
args << '--show-repo-map'
|
|
387
|
+
|
|
388
|
+
execute_command(args)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def show_prompts(options = {})
|
|
392
|
+
args = build_command_args(options)
|
|
393
|
+
args << '--show-prompts'
|
|
394
|
+
|
|
395
|
+
execute_command(args)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def list_models(provider = nil)
|
|
399
|
+
args = ['aider']
|
|
400
|
+
args << '--list-models' << provider if provider
|
|
401
|
+
|
|
402
|
+
execute_command(args)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def check_update
|
|
406
|
+
args = ['aider', '--check-update']
|
|
407
|
+
execute_command(args)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def upgrade
|
|
411
|
+
args = ['aider', '--upgrade']
|
|
412
|
+
execute_command(args)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Main client class
|
|
417
|
+
class Client
|
|
418
|
+
include ModelConfiguration
|
|
419
|
+
include OutputConfiguration
|
|
420
|
+
include GitConfiguration
|
|
421
|
+
include LintTestConfiguration
|
|
422
|
+
include GeneralConfiguration
|
|
423
|
+
include ConventionConfiguration
|
|
424
|
+
include ExecutionMethods
|
|
425
|
+
|
|
426
|
+
attr_reader :config, :files, :read_only_files
|
|
427
|
+
|
|
428
|
+
def initialize(options = {}, &block)
|
|
429
|
+
@config = Config::Configuration.new(options)
|
|
430
|
+
@files = []
|
|
431
|
+
@read_only_files = []
|
|
432
|
+
|
|
433
|
+
# Apply block configuration if provided
|
|
434
|
+
if block_given?
|
|
435
|
+
block.call(@config)
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Add files to edit (can take single file or array of files)
|
|
440
|
+
def add_files(file_path)
|
|
441
|
+
files_to_add = Array(file_path)
|
|
442
|
+
@files.concat(files_to_add)
|
|
443
|
+
self
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Add read-only files (can take single file or array of files)
|
|
447
|
+
def add_read_only_file(file_path)
|
|
448
|
+
files_to_add = Array(file_path)
|
|
449
|
+
@read_only_files.concat(files_to_add)
|
|
450
|
+
self
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Add entire folder (recursively finds all files)
|
|
454
|
+
def add_folder(folder_path, extensions: nil, exclude_patterns: [])
|
|
455
|
+
require 'find'
|
|
456
|
+
|
|
457
|
+
files_in_folder = []
|
|
458
|
+
|
|
459
|
+
Find.find(folder_path) do |path|
|
|
460
|
+
next if File.directory?(path)
|
|
461
|
+
|
|
462
|
+
# Skip if file doesn't match extension filter
|
|
463
|
+
if extensions
|
|
464
|
+
file_ext = File.extname(path)
|
|
465
|
+
next unless extensions.include?(file_ext)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Skip if file matches exclude patterns
|
|
469
|
+
skip_file = exclude_patterns.any? do |pattern|
|
|
470
|
+
case pattern
|
|
471
|
+
when String
|
|
472
|
+
path.include?(pattern)
|
|
473
|
+
when Regexp
|
|
474
|
+
path.match?(pattern)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
next if skip_file
|
|
478
|
+
|
|
479
|
+
files_in_folder << path
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
@files.concat(files_in_folder)
|
|
483
|
+
self
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Add folder as read-only context
|
|
487
|
+
def add_read_only_folder(folder_path, extensions: nil, exclude_patterns: [])
|
|
488
|
+
require 'find'
|
|
489
|
+
|
|
490
|
+
files_in_folder = []
|
|
491
|
+
|
|
492
|
+
Find.find(folder_path) do |path|
|
|
493
|
+
next if File.directory?(path)
|
|
494
|
+
|
|
495
|
+
# Skip if file doesn't match extension filter
|
|
496
|
+
if extensions
|
|
497
|
+
file_ext = File.extname(path)
|
|
498
|
+
next unless extensions.include?(file_ext)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Skip if file matches exclude patterns
|
|
502
|
+
skip_file = exclude_patterns.any? do |pattern|
|
|
503
|
+
case pattern
|
|
504
|
+
when String
|
|
505
|
+
path.include?(pattern)
|
|
506
|
+
when Regexp
|
|
507
|
+
path.match?(pattern)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
next if skip_file
|
|
511
|
+
|
|
512
|
+
files_in_folder << path
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
@read_only_files.concat(files_in_folder)
|
|
516
|
+
self
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
private
|
|
520
|
+
|
|
521
|
+
def build_command_args(options = {})
|
|
522
|
+
args = ['aider']
|
|
523
|
+
|
|
524
|
+
# Add config arguments
|
|
525
|
+
args.concat(@config.to_aider_args)
|
|
526
|
+
|
|
527
|
+
# Add files
|
|
528
|
+
@files.each { |file| args << '--file' << file }
|
|
529
|
+
@read_only_files.each { |file| args << '--read' << file }
|
|
530
|
+
|
|
531
|
+
# Add additional options
|
|
532
|
+
add_cli_options(args, options)
|
|
533
|
+
|
|
534
|
+
args
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def add_cli_options(args, options)
|
|
538
|
+
options.each do |key, value|
|
|
539
|
+
case key
|
|
540
|
+
when :config_file
|
|
541
|
+
args << '--config' << value
|
|
542
|
+
when :env_file
|
|
543
|
+
args << '--env-file' << value
|
|
544
|
+
when :dry_run
|
|
545
|
+
args << '--dry-run' if value
|
|
546
|
+
when :verbose
|
|
547
|
+
args << '--verbose' if value
|
|
548
|
+
when :yes_always
|
|
549
|
+
args << '--yes-always' if value
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def execute_command(args, interactive: false)
|
|
555
|
+
puts "Executing: #{args.join(' ')}" if @config.verbose
|
|
556
|
+
|
|
557
|
+
if interactive
|
|
558
|
+
# For interactive mode, we need to spawn a process that keeps running
|
|
559
|
+
spawn(*args)
|
|
560
|
+
else
|
|
561
|
+
# For non-interactive mode, capture output
|
|
562
|
+
stdout, stderr, status = Open3.capture3(*args)
|
|
563
|
+
|
|
564
|
+
raise Error, "Aider command failed: #{stderr}" unless status.success?
|
|
565
|
+
|
|
566
|
+
stdout
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
end
|