aia 0.9.11 → 0.9.12

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/CHANGELOG.md +66 -2
  4. data/README.md +133 -4
  5. data/docs/advanced-prompting.md +721 -0
  6. data/docs/cli-reference.md +582 -0
  7. data/docs/configuration.md +347 -0
  8. data/docs/contributing.md +332 -0
  9. data/docs/directives-reference.md +490 -0
  10. data/docs/examples/index.md +277 -0
  11. data/docs/examples/mcp/index.md +479 -0
  12. data/docs/examples/prompts/analysis/index.md +78 -0
  13. data/docs/examples/prompts/automation/index.md +108 -0
  14. data/docs/examples/prompts/development/index.md +125 -0
  15. data/docs/examples/prompts/index.md +333 -0
  16. data/docs/examples/prompts/learning/index.md +127 -0
  17. data/docs/examples/prompts/writing/index.md +62 -0
  18. data/docs/examples/tools/index.md +292 -0
  19. data/docs/faq.md +414 -0
  20. data/docs/guides/available-models.md +366 -0
  21. data/docs/guides/basic-usage.md +477 -0
  22. data/docs/guides/chat.md +474 -0
  23. data/docs/guides/executable-prompts.md +417 -0
  24. data/docs/guides/first-prompt.md +454 -0
  25. data/docs/guides/getting-started.md +455 -0
  26. data/docs/guides/image-generation.md +507 -0
  27. data/docs/guides/index.md +46 -0
  28. data/docs/guides/models.md +507 -0
  29. data/docs/guides/tools.md +856 -0
  30. data/docs/index.md +173 -0
  31. data/docs/installation.md +238 -0
  32. data/docs/mcp-integration.md +612 -0
  33. data/docs/prompt_management.md +579 -0
  34. data/docs/security.md +629 -0
  35. data/docs/tools-and-mcp-examples.md +1186 -0
  36. data/docs/workflows-and-pipelines.md +563 -0
  37. data/examples/tools/mcp/github_mcp_server.json +11 -0
  38. data/examples/tools/mcp/imcp.json +7 -0
  39. data/lib/aia/chat_processor_service.rb +19 -3
  40. data/lib/aia/config/base.rb +224 -0
  41. data/lib/aia/config/cli_parser.rb +409 -0
  42. data/lib/aia/config/defaults.rb +88 -0
  43. data/lib/aia/config/file_loader.rb +131 -0
  44. data/lib/aia/config/validator.rb +184 -0
  45. data/lib/aia/config.rb +10 -860
  46. data/lib/aia/directive_processor.rb +27 -372
  47. data/lib/aia/directives/configuration.rb +114 -0
  48. data/lib/aia/directives/execution.rb +37 -0
  49. data/lib/aia/directives/models.rb +178 -0
  50. data/lib/aia/directives/registry.rb +120 -0
  51. data/lib/aia/directives/utility.rb +70 -0
  52. data/lib/aia/directives/web_and_file.rb +71 -0
  53. data/lib/aia/prompt_handler.rb +23 -3
  54. data/lib/aia/ruby_llm_adapter.rb +307 -128
  55. data/lib/aia/session.rb +27 -14
  56. data/lib/aia/utility.rb +12 -8
  57. data/lib/aia.rb +11 -2
  58. data/lib/extensions/ruby_llm/.irbrc +56 -0
  59. data/mkdocs.yml +165 -0
  60. metadata +77 -20
  61. /data/{images → docs/assets/images}/aia.png +0 -0
data/lib/aia/config.rb CHANGED
@@ -5,876 +5,26 @@
5
5
  # for the AIA application. It provides methods to parse command-line
6
6
  # arguments, environment variables, and configuration files.
7
7
 
8
- require 'yaml'
9
- require 'toml-rb'
10
- require 'date'
11
- require 'erb'
12
- require 'optparse'
13
- require 'json'
14
- require 'tempfile'
15
- require 'fileutils'
8
+ require_relative 'config/base'
16
9
 
17
10
  module AIA
18
11
  class Config
19
- DEFAULT_CONFIG = OpenStruct.new({
20
- adapter: 'ruby_llm', # 'ruby_llm' or ???
21
- #
22
- aia_dir: File.join(ENV['HOME'], '.aia'),
23
- config_file: File.join(ENV['HOME'], '.aia', 'config.yml'),
24
- out_file: 'temp.md',
25
- log_file: File.join(ENV['HOME'], '.prompts', '_prompts.log'),
26
- context_files: [],
27
- #
28
- prompts_dir: File.join(ENV['HOME'], '.prompts'),
29
- prompt_extname: PromptManager::Storage::FileSystemAdapter::PROMPT_EXTENSION,
30
- #
31
- roles_prefix: 'roles',
32
- roles_dir: File.join(ENV['HOME'], '.prompts', 'roles'),
33
- role: '',
34
-
35
- #
36
- system_prompt: '',
37
-
38
- # Tools
39
- tools: '', # Comma-separated string of loaded tool names (set by adapter)
40
- allowed_tools: nil, # nil means all tools are allowed; otherwise an Array of Strings which are the tool names
41
- rejected_tools: nil, # nil means no tools are rejected
42
- tool_paths: [], # Strings - absolute and relative to tools
43
-
44
- # Flags
45
- markdown: true,
46
- shell: true,
47
- erb: true,
48
- chat: false,
49
- clear: false,
50
- terse: false,
51
- verbose: false,
52
- debug: $DEBUG_ME,
53
- fuzzy: false,
54
- speak: false,
55
- append: false, # Default to not append to existing out_file
56
-
57
- # workflow
58
- pipeline: [],
59
-
60
- # PromptManager::Prompt Tailoring
61
- parameter_regex: PromptManager::Prompt.parameter_regex.to_s,
62
-
63
- # LLM tuning parameters
64
- temperature: 0.7,
65
- max_tokens: 2048,
66
- top_p: 1.0,
67
- frequency_penalty: 0.0,
68
- presence_penalty: 0.0,
69
-
70
- # Audio Parameters
71
- voice: 'alloy',
72
- speak_command: 'afplay', # 'afplay' for audio files on MacOS
73
-
74
- # Image Parameters
75
- image_size: '1024x1024',
76
- image_quality: 'standard',
77
- image_style: 'vivid',
78
-
79
- # Models
80
- model: 'gpt-4o-mini',
81
- speech_model: 'tts-1',
82
- transcription_model: 'whisper-1',
83
- embedding_model: 'text-embedding-ada-002',
84
- image_model: 'dall-e-3',
85
-
86
- # Model Regristery
87
- refresh: 7, # days between refreshes of model info; 0 means every startup
88
- last_refresh: Date.today - 1,
89
-
90
- # Ruby libraries to require for Ruby binding
91
- require_libs: [],
92
- }).freeze
93
-
12
+ # Delegate all functionality to the modular config system
94
13
  def self.setup
95
- default_config = DEFAULT_CONFIG.dup
96
- cli_config = cli_options
97
- envar_config = envar_options(default_config, cli_config)
98
-
99
- file = envar_config.config_file unless envar_config.config_file.nil?
100
- file = cli_config.config_file unless cli_config.config_file.nil?
101
-
102
- cf_config = cf_options(file)
103
-
104
- config = OpenStruct.merge(
105
- default_config,
106
- cf_config || {},
107
- envar_config || {},
108
- cli_config || {}
109
- )
110
-
111
- tailor_the_config(config)
112
- load_libraries(config)
113
- load_tools(config)
114
-
115
- if config.dump_file
116
- dump_config(config, config.dump_file)
117
- end
118
-
119
- config
120
- end
121
-
122
-
123
- def self.tailor_the_config(config)
124
- remaining_args = config.remaining_args.dup
125
- config.remaining_args = nil
126
-
127
- stdin_content = process_stdin_content
128
- config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
129
-
130
- process_prompt_id_from_args(config, remaining_args)
131
- validate_and_set_context_files(config, remaining_args)
132
- handle_executable_prompt(config)
133
- validate_required_prompt_id(config)
134
- process_role_configuration(config)
135
- handle_fuzzy_search_prompt_id(config)
136
- normalize_boolean_flags(config)
137
- handle_completion_script(config)
138
- validate_final_prompt_requirements(config)
139
- configure_prompt_manager(config)
140
- prepare_pipeline(config)
141
- validate_pipeline_prompts(config)
142
-
143
- config
144
- end
145
-
146
-
147
- def self.load_libraries(config)
148
- return if config.require_libs.empty?
149
-
150
- exit_on_error = false
151
-
152
- config.require_libs.each do |library|
153
- begin
154
- require(library)
155
- rescue => e
156
- STDERR.puts "Error loading library '#{library}' #{e.message}"
157
- exit_on_error = true
158
- end
159
- end
160
-
161
- exit(1) if exit_on_error
162
-
163
- config
164
- end
165
-
166
-
167
- def self.load_tools(config)
168
- return if config.tool_paths.empty?
169
-
170
- require_all_tools(config)
171
-
172
- config
173
- end
174
-
175
-
176
- def self.require_all_tools(config)
177
- exit_on_error = false
178
-
179
- config.tool_paths.each do |tool_path|
180
- begin
181
- # expands path based on PWD
182
- absolute_tool_path = File.expand_path(tool_path)
183
- require(absolute_tool_path)
184
- rescue => e
185
- STDERR.puts "Error loading tool '#{tool_path}' #{e.message}"
186
- exit_on_error = true
187
- end
188
- end
189
-
190
- exit(1) if exit_on_error
191
- end
192
-
193
-
194
- # envar values are always String object so need other config
195
- # layers to know the prompter type for each key's value
196
- def self.envar_options(default, cli_config)
197
- config = OpenStruct.merge(default, cli_config)
198
- envars = ENV.keys.select { |key, _| key.start_with?('AIA_') }
199
- envars.each do |envar|
200
- key = envar.sub(/^AIA_/, '').downcase.to_sym
201
- value = ENV[envar]
202
-
203
- value = case config[key]
204
- when TrueClass, FalseClass
205
- value.downcase == 'true'
206
- when Integer
207
- value.to_i
208
- when Float
209
- value.to_f
210
- when Array
211
- value.split(',').map(&:strip)
212
- else
213
- value # defaults to String
214
- end
215
- config[key] = value
216
- end
217
-
218
- config
219
- end
220
-
221
-
222
- def self.cli_options
223
- config = OpenStruct.new
224
-
225
- begin
226
- opt_parser = create_option_parser(config)
227
- opt_parser.parse!
228
- rescue => e
229
- STDERR.puts "ERROR: #{e.message}"
230
- STDERR.puts " use --help for usage report"
231
- exit 1
232
- end
233
-
234
- parse_remaining_arguments(opt_parser, config)
235
- config
14
+ ConfigModules::Base.setup
236
15
  end
237
16
 
238
- def self.create_option_parser(config)
239
- OptionParser.new do |opts|
240
- setup_banner(opts)
241
- setup_mode_options(opts, config)
242
- setup_adapter_options(opts, config)
243
- setup_model_options(opts, config)
244
- setup_file_options(opts, config)
245
- setup_prompt_options(opts, config)
246
- setup_ai_parameters(opts, config)
247
- setup_audio_image_options(opts, config)
248
- setup_tool_options(opts, config)
249
- setup_utility_options(opts, config)
250
- end
251
- end
252
-
253
- def self.setup_banner(opts)
254
- opts.banner = "Usage: aia [options] [PROMPT_ID] [CONTEXT_FILE]*\n" +
255
- " aia --chat [PROMPT_ID] [CONTEXT_FILE]*\n" +
256
- " aia --chat [CONTEXT_FILE]*"
257
- end
258
-
259
- def self.setup_mode_options(opts, config)
260
- opts.on("--chat", "Begin a chat session with the LLM after processing all prompts in the pipeline.") do
261
- config.chat = true
262
- puts "Debug: Setting chat mode to true" if config.debug
263
- end
264
-
265
- opts.on("-f", "--fuzzy", "Use fuzzy matching for prompt search") do
266
- unless system("which fzf > /dev/null 2>&1")
267
- STDERR.puts "Error: 'fzf' is not installed. Please install 'fzf' to use the --fuzzy option."
268
- exit 1
269
- end
270
- config.fuzzy = true
271
- end
272
-
273
- opts.on("--terse", "Adds a special instruction to the prompt asking the AI to keep responses short and to the point") do
274
- config.terse = true
275
- end
276
- end
277
-
278
- def self.setup_adapter_options(opts, config)
279
- opts.on("--adapter ADAPTER", "Interface that adapts AIA to the LLM") do |adapter|
280
- adapter.downcase!
281
- valid_adapters = %w[ ruby_llm ] # NOTE: Add additional adapters here when needed
282
- if valid_adapters.include? adapter
283
- config.adapter = adapter
284
- else
285
- STDERR.puts "ERROR: Invalid adapter #{adapter} must be one of these: #{valid_adapters.join(', ')}"
286
- exit 1
287
- end
288
- end
289
-
290
- opts.on('--available_models [QUERY]', 'List (then exit) available models that match the optional query - a comma separated list of AND components like: openai,mini') do |query|
291
- list_available_models(query)
292
- end
293
- end
294
-
295
- def self.setup_model_options(opts, config)
296
- opts.on("-m MODEL", "--model MODEL", "Name of the LLM model to use") do |model|
297
- config.model = model
298
- end
299
-
300
- opts.on("--sm", "--speech_model MODEL", "Speech model to use") do |model|
301
- config.speech_model = model
302
- end
303
-
304
- opts.on("--tm", "--transcription_model MODEL", "Transcription model to use") do |model|
305
- config.transcription_model = model
306
- end
307
- end
308
-
309
- def self.setup_file_options(opts, config)
310
- opts.on("-c", "--config_file FILE", "Load config file") do |file|
311
- load_config_file(file, config)
312
- end
313
-
314
- opts.on("-o", "--[no-]out_file [FILE]", "Output file (default: temp.md)") do |file|
315
- if file == false # --no-out_file was used
316
- config.out_file = nil
317
- elsif file.nil? # No argument provided
318
- config.out_file = 'temp.md'
319
- else # File name provided
320
- config.out_file = File.expand_path(file, Dir.pwd)
321
- end
322
- end
323
-
324
- opts.on("-a", "--[no-]append", "Append to output file instead of overwriting") do |append|
325
- config.append = append
326
- end
327
-
328
- opts.on("-l", "--[no-]log_file [FILE]", "Log file") do |file|
329
- config.log_file = file
330
- end
331
-
332
- opts.on("--md", "--[no-]markdown", "Format with Markdown") do |md|
333
- config.markdown = md
334
- end
335
- end
336
-
337
- def self.setup_prompt_options(opts, config)
338
- opts.on("--prompts_dir DIR", "Directory containing prompt files") do |dir|
339
- config.prompts_dir = dir
340
- end
341
-
342
- opts.on("--roles_prefix PREFIX", "Subdirectory name for role files (default: roles)") do |prefix|
343
- config.roles_prefix = prefix
344
- end
345
-
346
- opts.on("-r", "--role ROLE_ID", "Role ID to prepend to prompt") do |role|
347
- config.role = role
348
- end
349
-
350
- opts.on("-n", "--next PROMPT_ID", "Next prompt to process") do |next_prompt|
351
- config.pipeline ||= []
352
- config.pipeline << next_prompt
353
- end
354
-
355
- opts.on("-p PROMPTS", "--pipeline PROMPTS", "Pipeline of comma-seperated prompt IDs to process") do |pipeline|
356
- config.pipeline ||= []
357
- config.pipeline += pipeline.split(',').map(&:strip)
358
- end
359
-
360
- opts.on("-x", "--[no-]exec", "Used to designate an executable prompt file") do |value|
361
- config.executable_prompt = value
362
- end
363
-
364
- opts.on("--system_prompt PROMPT_ID", "System prompt ID to use for chat sessions") do |prompt_id|
365
- config.system_prompt = prompt_id
366
- end
367
-
368
- opts.on('--regex pattern', 'Regex pattern to extract parameters from prompt text') do |pattern|
369
- config.parameter_regex = pattern
370
- end
371
- end
372
-
373
- def self.setup_ai_parameters(opts, config)
374
- opts.on("-t", "--temperature TEMP", Float, "Temperature for text generation") do |temp|
375
- config.temperature = temp
376
- end
377
-
378
- opts.on("--max_tokens TOKENS", Integer, "Maximum tokens for text generation") do |tokens|
379
- config.max_tokens = tokens
380
- end
381
-
382
- opts.on("--top_p VALUE", Float, "Top-p sampling value") do |value|
383
- config.top_p = value
384
- end
385
-
386
- opts.on("--frequency_penalty VALUE", Float, "Frequency penalty") do |value|
387
- config.frequency_penalty = value
388
- end
389
-
390
- opts.on("--presence_penalty VALUE", Float, "Presence penalty") do |value|
391
- config.presence_penalty = value
392
- end
393
- end
394
-
395
- def self.setup_audio_image_options(opts, config)
396
- opts.on("--speak", "Simple implementation. Uses the speech model to convert text to audio, then plays the audio. Fun with --chat. Supports configuration of speech model and voice.") do
397
- config.speak = true
398
- end
399
-
400
- opts.on("--voice VOICE", "Voice to use for speech") do |voice|
401
- config.voice = voice
402
- end
403
-
404
- opts.on("--is", "--image_size SIZE", "Image size for image generation") do |size|
405
- config.image_size = size
406
- end
407
-
408
- opts.on("--iq", "--image_quality QUALITY", "Image quality for image generation") do |quality|
409
- config.image_quality = quality
410
- end
411
-
412
- opts.on("--style", "--image_style STYLE", "Style for image generation") do |style|
413
- config.image_style = style
414
- end
415
- end
416
-
417
- def self.setup_tool_options(opts, config)
418
- opts.on("--rq LIBS", "--require LIBS", "Ruby libraries to require for Ruby directive") do |libs|
419
- config.require_libs ||= []
420
- config.require_libs += libs.split(',')
421
- end
422
-
423
- opts.on("--tools PATH_LIST", "Add a tool(s)") do |a_path_list|
424
- process_tools_option(a_path_list, config)
425
- end
426
-
427
- opts.on("--at", "--allowed_tools TOOLS_LIST", "Allow only these tools to be used") do |tools_list|
428
- process_allowed_tools_option(tools_list, config)
429
- end
430
-
431
- opts.on("--rt", "--rejected_tools TOOLS_LIST", "Reject these tools") do |tools_list|
432
- process_rejected_tools_option(tools_list, config)
433
- end
434
- end
435
-
436
- def self.setup_utility_options(opts, config)
437
- opts.on("-d", "--debug", "Enable debug output") do
438
- config.debug = $DEBUG_ME = true
439
- end
440
-
441
- opts.on("--no-debug", "Disable debug output") do
442
- config.debug = $DEBUG_ME = false
443
- end
444
-
445
- opts.on("-v", "--[no-]verbose", "Be verbose") do |value|
446
- config.verbose = value
447
- end
448
-
449
- opts.on("--refresh DAYS", Integer, "Refresh models database interval in days") do |days|
450
- config.refresh = days || 0
451
- end
452
-
453
- opts.on("--dump FILE", "Dump config to file") do |file|
454
- config.dump_file = file
455
- end
456
-
457
- opts.on("--completion SHELL", "Show completion script for bash|zsh|fish - default is nil") do |shell|
458
- config.completion = shell
459
- end
460
-
461
- opts.on("--version", "Show version") do
462
- puts AIA::VERSION
463
- exit
464
- end
465
-
466
- opts.on("-h", "--help", "Prints this help") do
467
- puts <<~HELP
468
-
469
- AIA your AI Assistant
470
- - designed for generative AI workflows,
471
- - effortlessly manage AI prompts,
472
- - integrate seamlessly with shell and embedded Ruby (ERB),
473
- - run batch processes,
474
- - engage in interactive chats,
475
- - with user defined directives, tools and MCP clients.
476
-
477
- HELP
478
-
479
- puts opts
480
-
481
- puts <<~EXTRA
482
-
483
- Explore Further:
484
- - AIA Report an Issue: https://github.com/MadBomber/aia/issues
485
- - AIA Documentation: https://github.com/madbomber/aia/blob/main/README.md
486
- - AIA GitHub Repository: https://github.com/MadBomber/aia
487
- - PromptManager Docs: https://github.com/MadBomber/prompt_manager/blob/main/README.md
488
- - ERB Documentation: https://rubyapi.org/o/erb
489
- - RubyLLM Tool Docs: https://rubyllm.com/guides/tools
490
- - MCP Client Docs: https://github.com/patvice/ruby_llm-mcp/blob/main/README.md
491
-
492
- EXTRA
493
-
494
- exit
495
- end
496
- end
497
-
498
- def self.list_available_models(query)
499
- # SMELL: mostly duplications the code in the vailable_models directive
500
- # assumes that the adapter is for the ruby_llm gem
501
- # should this be moved to the Utilities class as a common method?
502
-
503
- if query.nil?
504
- query = []
505
- else
506
- query = query.split(',')
507
- end
508
-
509
- header = "\nAvailable LLMs"
510
- header += " for #{query.join(' and ')}" if query
511
-
512
- puts header + ':'
513
- puts
514
-
515
- q1 = query.select{|q| q.include?('_to_')}.map{|q| ':'==q[0] ? q[1...] : q}
516
- q2 = query.reject{|q| q.include?('_to_')}
517
-
518
- counter = 0
519
-
520
- RubyLLM.models.all.each do |llm|
521
- inputs = llm.modalities.input.join(',')
522
- outputs = llm.modalities.output.join(',')
523
- entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}"
524
-
525
- if query.nil? || query.empty?
526
- counter += 1
527
- puts entry
528
- next
529
- end
530
-
531
- show_it = true
532
- q1.each{|q| show_it &&= llm.modalities.send("#{q}?")}
533
- q2.each{|q| show_it &&= entry.include?(q)}
534
-
535
- if show_it
536
- counter += 1
537
- puts entry
538
- end
539
- end
540
-
541
- puts if counter > 0
542
- puts "#{counter} LLMs matching your query"
543
- puts
544
-
545
- exit
546
- end
547
-
548
- def self.load_config_file(file, config)
549
- if File.exist?(file)
550
- ext = File.extname(file).downcase
551
- content = File.read(file)
552
-
553
- # Process ERB if filename ends with .erb
554
- if file.end_with?('.erb')
555
- content = ERB.new(content).result
556
- file = file.chomp('.erb')
557
- File.write(file, content)
558
- end
559
-
560
- file_config = case ext
561
- when '.yml', '.yaml'
562
- YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
563
- when '.toml'
564
- TomlRB.parse(content)
565
- else
566
- raise "Unsupported config file format: #{ext}"
567
- end
568
-
569
- file_config.each do |key, value|
570
- config[key.to_sym] = value
571
- end
572
- else
573
- raise "Config file not found: #{file}"
574
- end
575
- end
576
-
577
- def self.process_tools_option(a_path_list, config)
578
- config.tool_paths ||= []
579
-
580
- if a_path_list.empty?
581
- STDERR.puts "No list of paths for --tools option"
582
- exit 1
583
- else
584
- paths = a_path_list.split(',').map(&:strip).uniq
585
- end
586
-
587
- paths.each do |a_path|
588
- if File.exist?(a_path)
589
- if File.file?(a_path)
590
- if '.rb' == File.extname(a_path)
591
- config.tool_paths << a_path
592
- else
593
- STDERR.puts "file should have *.rb extension: #{a_path}"
594
- exit 1
595
- end
596
- elsif File.directory?(a_path)
597
- rb_files = Dir.glob(File.join(a_path, '*.rb'))
598
- config.tool_paths += rb_files
599
- end
600
- else
601
- STDERR.puts "file/dir path is not valid: #{a_path}"
602
- exit 1
603
- end
604
- end
605
-
606
- config.tool_paths.uniq!
607
- end
608
-
609
- def self.process_allowed_tools_option(tools_list, config)
610
- config.allowed_tools ||= []
611
- if tools_list.empty?
612
- STDERR.puts "No list of tool names provided for --allowed_tools option"
613
- exit 1
614
- else
615
- config.allowed_tools += tools_list.split(',').map(&:strip)
616
- config.allowed_tools.uniq!
617
- end
618
- end
619
-
620
- def self.process_rejected_tools_option(tools_list, config)
621
- config.rejected_tools ||= []
622
- if tools_list.empty?
623
- STDERR.puts "No list of tool names provided for --rejected_tools option"
624
- exit 1
625
- else
626
- config.rejected_tools += tools_list.split(',').map(&:strip)
627
- config.rejected_tools.uniq!
628
- end
629
- end
630
-
631
- def self.process_stdin_content
632
- stdin_content = ''
633
-
634
- if !STDIN.tty? && !STDIN.closed?
635
- begin
636
- stdin_content << "\n" + STDIN.read
637
- STDIN.reopen('/dev/tty') # Reopen STDIN for interactive use
638
- rescue => _
639
- # If we can't reopen, continue without error
640
- end
641
- end
642
-
643
- stdin_content
644
- end
645
-
646
- def self.process_prompt_id_from_args(config, remaining_args)
647
- return if remaining_args.empty?
648
-
649
- maybe_id = remaining_args.first
650
- maybe_id_plus = File.join(config.prompts_dir, maybe_id + config.prompt_extname)
651
-
652
- if AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
653
- config.prompt_id = remaining_args.shift
654
- end
655
- end
656
-
657
- def self.validate_and_set_context_files(config, remaining_args)
658
- return if remaining_args.empty?
659
-
660
- bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
661
- if bad_files.any?
662
- STDERR.puts "Error: The following files do not exist: #{bad_files.join(', ')}"
663
- exit 1
664
- end
665
-
666
- config.context_files ||= []
667
- config.context_files += remaining_args
668
- end
669
-
670
- def self.handle_executable_prompt(config)
671
- return unless config.executable_prompt && config.context_files && !config.context_files.empty?
672
-
673
- config.executable_prompt_file = config.context_files.pop
674
- end
675
-
676
- def self.validate_required_prompt_id(config)
677
- return unless config.prompt_id.nil? && !config.chat && !config.fuzzy
678
-
679
- STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
680
- exit 1
681
- end
682
-
683
- def self.process_role_configuration(config)
684
- return if config.role.empty?
685
-
686
- unless config.roles_prefix.empty?
687
- unless config.role.start_with?(config.roles_prefix)
688
- config.role.prepend "#{config.roles_prefix}/"
689
- end
690
- end
691
-
692
- config.roles_dir ||= File.join(config.prompts_dir, config.roles_prefix)
693
-
694
- if config.prompt_id.nil? || config.prompt_id.empty?
695
- if !config.role.nil? && !config.role.empty?
696
- config.prompt_id = config.role
697
- config.pipeline.prepend config.prompt_id
698
- config.role = ''
699
- end
700
- end
701
- end
702
-
703
- def self.handle_fuzzy_search_prompt_id(config)
704
- return unless config.fuzzy && config.prompt_id.empty?
705
-
706
- # When fuzzy search is enabled but no prompt ID is provided,
707
- # set a special value to trigger fuzzy search without an initial query
708
- # SMELL: This feels like a cludge
709
- config.prompt_id = '__FUZZY_SEARCH__'
710
- end
711
-
712
- def self.normalize_boolean_flags(config)
713
- normalize_boolean_flag(config, :chat)
714
- normalize_boolean_flag(config, :fuzzy)
715
- end
716
-
717
- def self.normalize_boolean_flag(config, flag)
718
- return if [TrueClass, FalseClass].include?(config[flag].class)
719
-
720
- config[flag] = if config[flag].nil? || config[flag].empty?
721
- false
722
- else
723
- true
724
- end
725
- end
726
-
727
- def self.handle_completion_script(config)
728
- return unless config.completion
729
-
730
- generate_completion_script(config.completion)
731
- exit
732
- end
733
-
734
- def self.validate_final_prompt_requirements(config)
735
- # Only require a prompt_id if we're not in chat mode, not using fuzzy search, and no context files
736
- if !config.chat && !config.fuzzy && (config.prompt_id.nil? || config.prompt_id.empty?) && (!config.context_files || config.context_files.empty?)
737
- STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
738
- exit 1
739
- end
740
-
741
- # If we're in chat mode with context files but no prompt_id, that's valid
742
- # This is handled implicitly - no action needed
743
- end
744
-
745
- def self.configure_prompt_manager(config)
746
- return unless config.parameter_regex
747
-
748
- PromptManager::Prompt.parameter_regex = Regexp.new(config.parameter_regex)
749
- end
750
-
751
- def self.prepare_pipeline(config)
752
- return if config.prompt_id.nil? || config.prompt_id.empty? || config.prompt_id == config.pipeline.first
753
-
754
- config.pipeline.prepend config.prompt_id
755
- end
756
-
757
- def self.validate_pipeline_prompts(config)
758
- return if config.pipeline.empty?
759
-
760
- and_exit = false
761
-
762
- config.pipeline.each do |prompt_id|
763
- # Skip empty prompt IDs (can happen in chat-only mode)
764
- next if prompt_id.nil? || prompt_id.empty?
765
-
766
- prompt_file_path = File.join(config.prompts_dir, "#{prompt_id}.txt")
767
- unless File.exist?(prompt_file_path)
768
- STDERR.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
769
- and_exit = true
770
- end
771
- end
772
-
773
- exit(1) if and_exit
774
- end
775
-
776
- def self.parse_remaining_arguments(opt_parser, config)
777
- args = ARGV.dup
778
-
779
- # Parse the command line arguments
780
- begin
781
- config.remaining_args = opt_parser.parse(args)
782
- rescue OptionParser::InvalidOption => e
783
- puts e.message
784
- puts opt_parser
785
- exit 1
786
- end
787
- end
788
-
789
-
790
- def self.cf_options(file)
791
- config = OpenStruct.new
792
-
793
- if File.exist?(file)
794
- content = read_and_process_config_file(file)
795
- file_config = parse_config_content(content, File.extname(file).downcase)
796
- apply_file_config_to_struct(config, file_config)
797
- else
798
- STDERR.puts "WARNING:Config file not found: #{file}"
799
- end
800
-
801
- normalize_last_refresh_date(config)
802
- config
803
- end
804
-
805
- def self.read_and_process_config_file(file)
806
- content = File.read(file)
807
-
808
- # Process ERB if filename ends with .erb
809
- if file.end_with?('.erb')
810
- content = ERB.new(content).result
811
- processed_file = file.chomp('.erb')
812
- File.write(processed_file, content)
813
- end
814
-
815
- content
816
- end
817
-
818
- def self.parse_config_content(content, ext)
819
- case ext
820
- when '.yml', '.yaml'
821
- YAML.safe_load(content, permitted_classes: [Symbol], symbolize_names: true)
822
- when '.toml'
823
- TomlRB.parse(content)
824
- else
825
- raise "Unsupported config file format: #{ext}"
826
- end
827
- end
828
-
829
- def self.apply_file_config_to_struct(config, file_config)
830
- file_config.each do |key, value|
831
- config[key] = value
832
- end
833
- end
834
-
835
- def self.normalize_last_refresh_date(config)
836
- return unless config.last_refresh&.is_a?(String)
837
-
838
- config.last_refresh = Date.strptime(config.last_refresh, '%Y-%m-%d')
839
- end
840
-
841
-
842
- def self.generate_completion_script(shell)
843
- script_path = File.join(File.dirname(__FILE__), "aia_completion.#{shell}")
844
-
845
- if File.exist?(script_path)
846
- puts File.read(script_path)
17
+ # Maintain backward compatibility by delegating to Base module
18
+ def self.method_missing(method_name, *args, &block)
19
+ if ConfigModules::Base.respond_to?(method_name)
20
+ ConfigModules::Base.send(method_name, *args, &block)
847
21
  else
848
- STDERR.puts "ERROR: The shell '#{shell}' is not supported or the completion script is missing."
22
+ super
849
23
  end
850
24
  end
851
25
 
852
-
853
- def self.dump_config(config, file)
854
- # Implementation for config dump
855
- ext = File.extname(file).downcase
856
-
857
- config.last_refresh = config.last_refresh.to_s if config.last_refresh.is_a? Date
858
-
859
- config_hash = config.to_h
860
-
861
- # Remove prompt_id to prevent automatic initial pompting in --chat mode
862
- config_hash.delete(:prompt_id)
863
-
864
- # Remove dump_file key to prevent automatic exit on next load
865
- config_hash.delete(:dump_file)
866
-
867
- content = case ext
868
- when '.yml', '.yaml'
869
- YAML.dump(config_hash)
870
- when '.toml'
871
- TomlRB.dump(config_hash)
872
- else
873
- raise "Unsupported config file format: #{ext}"
874
- end
875
-
876
- File.write(file, content)
877
- puts "Config successfully dumped to #{file}"
26
+ def self.respond_to_missing?(method_name, include_private = false)
27
+ ConfigModules::Base.respond_to?(method_name, include_private) || super
878
28
  end
879
29
  end
880
30
  end