aia 0.9.24 → 0.10.2

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/.version +1 -1
  3. data/CHANGELOG.md +84 -3
  4. data/README.md +179 -59
  5. data/bin/aia +6 -0
  6. data/docs/cli-reference.md +145 -72
  7. data/docs/configuration.md +156 -19
  8. data/docs/examples/tools/index.md +2 -2
  9. data/docs/faq.md +11 -11
  10. data/docs/guides/available-models.md +11 -11
  11. data/docs/guides/basic-usage.md +18 -17
  12. data/docs/guides/chat.md +57 -11
  13. data/docs/guides/executable-prompts.md +15 -15
  14. data/docs/guides/first-prompt.md +2 -2
  15. data/docs/guides/getting-started.md +6 -6
  16. data/docs/guides/image-generation.md +24 -24
  17. data/docs/guides/local-models.md +2 -2
  18. data/docs/guides/models.md +96 -18
  19. data/docs/guides/tools.md +4 -4
  20. data/docs/installation.md +2 -2
  21. data/docs/prompt_management.md +11 -11
  22. data/docs/security.md +3 -3
  23. data/docs/workflows-and-pipelines.md +1 -1
  24. data/examples/README.md +6 -6
  25. data/examples/headlines +3 -3
  26. data/lib/aia/aia_completion.bash +2 -2
  27. data/lib/aia/aia_completion.fish +4 -4
  28. data/lib/aia/aia_completion.zsh +2 -2
  29. data/lib/aia/chat_processor_service.rb +31 -21
  30. data/lib/aia/config/cli_parser.rb +403 -403
  31. data/lib/aia/config/config_section.rb +87 -0
  32. data/lib/aia/config/defaults.yml +219 -0
  33. data/lib/aia/config/defaults_loader.rb +147 -0
  34. data/lib/aia/config/mcp_parser.rb +151 -0
  35. data/lib/aia/config/model_spec.rb +67 -0
  36. data/lib/aia/config/validator.rb +185 -136
  37. data/lib/aia/config.rb +336 -17
  38. data/lib/aia/directive_processor.rb +14 -6
  39. data/lib/aia/directives/configuration.rb +24 -10
  40. data/lib/aia/directives/models.rb +3 -4
  41. data/lib/aia/directives/utility.rb +3 -2
  42. data/lib/aia/directives/web_and_file.rb +50 -47
  43. data/lib/aia/logger.rb +328 -0
  44. data/lib/aia/prompt_handler.rb +18 -22
  45. data/lib/aia/ruby_llm_adapter.rb +572 -69
  46. data/lib/aia/session.rb +9 -8
  47. data/lib/aia/ui_presenter.rb +20 -16
  48. data/lib/aia/utility.rb +50 -18
  49. data/lib/aia.rb +91 -66
  50. data/lib/extensions/ruby_llm/modalities.rb +2 -0
  51. data/mcp_servers/apple-mcp.json +8 -0
  52. data/mcp_servers/mcp_server_chart.json +11 -0
  53. data/mcp_servers/playwright_one.json +8 -0
  54. data/mcp_servers/playwright_two.json +8 -0
  55. data/mcp_servers/tavily_mcp_server.json +8 -0
  56. metadata +83 -25
  57. data/lib/aia/config/base.rb +0 -308
  58. data/lib/aia/config/defaults.rb +0 -91
  59. data/lib/aia/config/file_loader.rb +0 -163
  60. data/mcp_servers/imcp.json +0 -7
  61. data/mcp_servers/launcher.json +0 -11
  62. data/mcp_servers/timeserver.json +0 -8
@@ -1,183 +1,232 @@
1
- # lib/aia/config/validator.rb
1
+ # frozen_string_literal: true
2
2
 
3
- require 'ostruct'
3
+ # lib/aia/config/validator.rb
4
+ #
5
+ # Validates and tailors configuration after it's been loaded.
6
+ # Handles prompt ID extraction, context file validation, role processing, etc.
4
7
 
5
8
  module AIA
6
- module ConfigModules
7
- module Validator
8
- class << self
9
- def tailor_the_config(config)
10
- remaining_args = config.remaining_args.dup
11
- config.remaining_args = nil
12
-
13
- stdin_content = process_stdin_content
14
- config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
15
-
16
- process_prompt_id_from_args(config, remaining_args)
17
- validate_and_set_context_files(config, remaining_args)
18
- handle_executable_prompt(config)
19
- validate_required_prompt_id(config)
20
- process_role_configuration(config)
21
- handle_fuzzy_search_prompt_id(config)
22
- normalize_boolean_flags(config)
23
- handle_completion_script(config)
24
- validate_final_prompt_requirements(config)
25
- configure_prompt_manager(config)
26
- prepare_pipeline(config)
27
- validate_pipeline_prompts(config)
28
-
29
- config
30
- end
9
+ module ConfigValidator
10
+ class << self
11
+ # Tailor and validate the configuration
12
+ #
13
+ # @param config [AIA::Config] the configuration to validate
14
+ # @return [AIA::Config] the validated configuration
15
+ def tailor(config)
16
+ remaining_args = config.remaining_args&.dup || []
17
+ config.remaining_args = nil
18
+
19
+ # Process STDIN content if available
20
+ stdin_content = process_stdin_content
21
+ config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
22
+
23
+ # Process arguments and validate
24
+ process_prompt_id_from_args(config, remaining_args)
25
+ validate_and_set_context_files(config, remaining_args)
26
+ handle_executable_prompt(config)
27
+ handle_dump_config(config)
28
+ validate_required_prompt_id(config)
29
+ process_role_configuration(config)
30
+ handle_fuzzy_search_prompt_id(config)
31
+ normalize_boolean_flags(config)
32
+ handle_completion_script(config)
33
+ validate_final_prompt_requirements(config)
34
+ configure_prompt_manager(config)
35
+ prepare_pipeline(config)
36
+ validate_pipeline_prompts(config)
37
+
38
+ config
39
+ end
31
40
 
32
- def process_stdin_content
33
- stdin_content = ''
41
+ def process_stdin_content
42
+ stdin_content = ''
34
43
 
35
- if !STDIN.tty? && !STDIN.closed?
36
- begin
37
- stdin_content << "\n" + STDIN.read
38
- STDIN.reopen('/dev/tty') # Reopen STDIN for interactive use
39
- rescue => _
40
- # If we can't reopen, continue without error
41
- end
44
+ if !STDIN.tty? && !STDIN.closed?
45
+ begin
46
+ stdin_content << "\n" + STDIN.read
47
+ STDIN.reopen('/dev/tty')
48
+ rescue => _
49
+ # If we can't reopen, continue without error
42
50
  end
43
-
44
- stdin_content
45
51
  end
46
52
 
47
- def process_prompt_id_from_args(config, remaining_args)
48
- return if remaining_args.empty?
53
+ stdin_content
54
+ end
49
55
 
50
- maybe_id = remaining_args.first
51
- maybe_id_plus = File.join(config.prompts_dir, maybe_id + config.prompt_extname)
56
+ def process_prompt_id_from_args(config, remaining_args)
57
+ return if remaining_args.empty?
52
58
 
53
- if AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
54
- config.prompt_id = remaining_args.shift
55
- end
56
- end
59
+ maybe_id = remaining_args.first
60
+ maybe_id_plus = File.join(config.prompts.dir, maybe_id + config.prompts.extname)
57
61
 
58
- def validate_and_set_context_files(config, remaining_args)
59
- return if remaining_args.empty?
62
+ if AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
63
+ config.prompt_id = remaining_args.shift
64
+ end
65
+ end
60
66
 
61
- bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
62
- if bad_files.any?
63
- STDERR.puts "Error: The following files do not exist: #{bad_files.join(', ')}"
64
- exit 1
65
- end
67
+ def validate_and_set_context_files(config, remaining_args)
68
+ return if remaining_args.empty?
66
69
 
67
- config.context_files ||= []
68
- config.context_files += remaining_args
70
+ bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
71
+ if bad_files.any?
72
+ STDERR.puts "Error: The following files do not exist: #{bad_files.join(', ')}"
73
+ exit 1
69
74
  end
70
75
 
71
- def handle_executable_prompt(config)
72
- return unless config.executable_prompt && config.context_files && !config.context_files.empty?
76
+ config.context_files ||= []
77
+ config.context_files += remaining_args
78
+ end
73
79
 
74
- config.executable_prompt_file = config.context_files.pop
75
- end
80
+ def handle_executable_prompt(config)
81
+ return unless config.executable_prompt && config.context_files && !config.context_files.empty?
76
82
 
77
- def validate_required_prompt_id(config)
78
- return unless config.prompt_id.nil? && !config.chat && !config.fuzzy
83
+ config.executable_prompt_file = config.context_files.pop
84
+ end
79
85
 
80
- STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
81
- exit 1
82
- end
86
+ def validate_required_prompt_id(config)
87
+ return unless config.prompt_id.nil? && !(config.flags.chat == true) && !(config.flags.fuzzy == true)
88
+
89
+ STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
90
+ exit 1
91
+ end
83
92
 
84
- def process_role_configuration(config)
85
- return if config.role.empty?
93
+ def process_role_configuration(config)
94
+ role = config.prompts.role
95
+ return if role.nil? || role.empty?
86
96
 
87
- unless config.roles_prefix.empty?
88
- unless config.role.start_with?(config.roles_prefix)
89
- config.role.prepend "#{config.roles_prefix}/"
90
- end
97
+ roles_prefix = config.prompts.roles_prefix
98
+ unless roles_prefix.nil? || roles_prefix.empty?
99
+ unless role.start_with?(roles_prefix)
100
+ config.prompts.role = "#{roles_prefix}/#{role}"
101
+ role = config.prompts.role
91
102
  end
103
+ end
92
104
 
93
- config.roles_dir ||= File.join(config.prompts_dir, config.roles_prefix)
105
+ config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix)
94
106
 
95
- if config.prompt_id.nil? || config.prompt_id.empty?
96
- if !config.role.nil? && !config.role.empty?
97
- config.prompt_id = config.role
98
- config.pipeline.prepend config.prompt_id
99
- config.role = ''
100
- end
107
+ if config.prompt_id.nil? || config.prompt_id.empty?
108
+ unless role.nil? || role.empty?
109
+ config.prompt_id = role
110
+ config.pipeline.prepend(config.prompt_id)
111
+ config.prompts.role = ''
101
112
  end
102
113
  end
114
+ end
103
115
 
104
- def handle_fuzzy_search_prompt_id(config)
105
- return unless config.fuzzy && config.prompt_id.empty?
116
+ def handle_fuzzy_search_prompt_id(config)
117
+ return unless (config.flags.fuzzy == true) && (config.prompt_id.nil? || config.prompt_id.empty?)
106
118
 
107
- # When fuzzy search is enabled but no prompt ID is provided,
108
- # set a special value to trigger fuzzy search without an initial query
109
- # SMELL: This feels like a cludge
110
- config.prompt_id = '__FUZZY_SEARCH__'
111
- end
119
+ config.prompt_id = '__FUZZY_SEARCH__'
120
+ end
112
121
 
113
- def normalize_boolean_flags(config)
114
- normalize_boolean_flag(config, :chat)
115
- normalize_boolean_flag(config, :fuzzy)
116
- normalize_boolean_flag(config, :consensus)
117
- end
122
+ def normalize_boolean_flags(config)
123
+ normalize_boolean_flag(config.flags, :chat)
124
+ normalize_boolean_flag(config.flags, :fuzzy)
125
+ normalize_boolean_flag(config.flags, :consensus)
126
+ end
118
127
 
119
- def normalize_boolean_flag(config, flag)
120
- return if [TrueClass, FalseClass].include?(config[flag].class)
121
-
122
- config[flag] = case config[flag]
123
- when nil, '', 'false', false
124
- false
125
- when 'true', true
126
- true
127
- else
128
- # For any other non-empty string value, treat as true
129
- true
130
- end
131
- end
128
+ def normalize_boolean_flag(flags_section, flag)
129
+ value = flags_section.send(flag)
130
+ return if [TrueClass, FalseClass].include?(value.class)
132
131
 
133
- def handle_completion_script(config)
134
- return unless config.completion
132
+ normalized = case value
133
+ when nil, '', 'false', false
134
+ false
135
+ when 'true', true
136
+ true
137
+ else
138
+ true
139
+ end
135
140
 
136
- FileLoader.generate_completion_script(config.completion)
137
- exit
138
- end
141
+ flags_section.send("#{flag}=", normalized)
142
+ end
139
143
 
140
- def validate_final_prompt_requirements(config)
141
- # Only require a prompt_id if we're not in chat mode, not using fuzzy search, and no context files
142
- if !config.chat && !config.fuzzy && (config.prompt_id.nil? || config.prompt_id.empty?) && (!config.context_files || config.context_files.empty?)
143
- STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
144
- exit 1
145
- end
144
+ def handle_dump_config(config)
145
+ return unless config.dump_file
146
146
 
147
- # If we're in chat mode with context files but no prompt_id, that's valid
148
- # This is handled implicitly - no action needed
149
- end
147
+ dump_config(config, config.dump_file)
148
+ exit 0
149
+ end
150
150
 
151
- def configure_prompt_manager(config)
152
- return unless config.parameter_regex
151
+ def handle_completion_script(config)
152
+ return unless config.completion
153
153
 
154
- PromptManager::Prompt.parameter_regex = Regexp.new(config.parameter_regex)
155
- end
154
+ generate_completion_script(config.completion)
155
+ exit
156
+ end
156
157
 
157
- def prepare_pipeline(config)
158
- return if config.prompt_id.nil? || config.prompt_id.empty? || config.prompt_id == config.pipeline.first
158
+ def generate_completion_script(shell)
159
+ script_path = File.join(File.dirname(__FILE__), "../../aia_completion.#{shell}")
159
160
 
160
- config.pipeline.prepend config.prompt_id
161
+ if File.exist?(script_path)
162
+ puts File.read(script_path)
163
+ else
164
+ STDERR.puts "ERROR: The shell '#{shell}' is not supported or the completion script is missing."
161
165
  end
166
+ end
162
167
 
163
- def validate_pipeline_prompts(config)
164
- return if config.pipeline.empty?
168
+ def validate_final_prompt_requirements(config)
169
+ chat_mode = config.flags.chat == true
170
+ fuzzy_mode = config.flags.fuzzy == true
171
+ if !chat_mode && !fuzzy_mode && (config.prompt_id.nil? || config.prompt_id.empty?) && (config.context_files.nil? || config.context_files.empty?)
172
+ STDERR.puts "Error: A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
173
+ exit 1
174
+ end
175
+ end
165
176
 
166
- and_exit = false
177
+ def configure_prompt_manager(config)
178
+ return unless config.prompts.parameter_regex
167
179
 
168
- config.pipeline.each do |prompt_id|
169
- # Skip empty prompt IDs (can happen in chat-only mode)
170
- next if prompt_id.nil? || prompt_id.empty?
180
+ PromptManager::Prompt.parameter_regex = Regexp.new(config.prompts.parameter_regex)
181
+ end
171
182
 
172
- prompt_file_path = File.join(config.prompts_dir, "#{prompt_id}.txt")
173
- unless File.exist?(prompt_file_path)
174
- STDERR.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
175
- and_exit = true
176
- end
177
- end
183
+ def prepare_pipeline(config)
184
+ return if config.prompt_id.nil? || config.prompt_id.empty? || config.prompt_id == config.pipeline.first
185
+
186
+ config.pipeline.prepend(config.prompt_id)
187
+ end
188
+
189
+ def validate_pipeline_prompts(config)
190
+ return if config.pipeline.empty?
178
191
 
179
- exit(1) if and_exit
192
+ and_exit = false
193
+
194
+ config.pipeline.each do |prompt_id|
195
+ next if prompt_id.nil? || prompt_id.empty?
196
+
197
+ prompt_file_path = File.join(config.prompts.dir, "#{prompt_id}.txt")
198
+ unless File.exist?(prompt_file_path)
199
+ STDERR.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
200
+ and_exit = true
201
+ end
180
202
  end
203
+
204
+ exit(1) if and_exit
205
+ end
206
+
207
+ # Dump configuration to file
208
+ #
209
+ # @param config [AIA::Config] the configuration to dump
210
+ # @param file [String] the file path to dump to
211
+ def dump_config(config, file)
212
+ ext = File.extname(file).downcase
213
+
214
+ config_hash = config.to_h
215
+
216
+ # Remove runtime keys
217
+ config_hash.delete(:prompt_id)
218
+ config_hash.delete(:dump_file)
219
+
220
+ content = case ext
221
+ when '.yml', '.yaml'
222
+ require 'yaml'
223
+ YAML.dump(config_hash.transform_keys(&:to_s))
224
+ else
225
+ raise "Unsupported config file format: #{ext}. Use .yml or .yaml"
226
+ end
227
+
228
+ File.write(file, content)
229
+ puts "Config successfully dumped to #{file}"
181
230
  end
182
231
  end
183
232
  end