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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/CHANGELOG.md +84 -3
- data/README.md +179 -59
- data/bin/aia +6 -0
- data/docs/cli-reference.md +145 -72
- data/docs/configuration.md +156 -19
- data/docs/examples/tools/index.md +2 -2
- data/docs/faq.md +11 -11
- data/docs/guides/available-models.md +11 -11
- data/docs/guides/basic-usage.md +18 -17
- data/docs/guides/chat.md +57 -11
- data/docs/guides/executable-prompts.md +15 -15
- data/docs/guides/first-prompt.md +2 -2
- data/docs/guides/getting-started.md +6 -6
- data/docs/guides/image-generation.md +24 -24
- data/docs/guides/local-models.md +2 -2
- data/docs/guides/models.md +96 -18
- data/docs/guides/tools.md +4 -4
- data/docs/installation.md +2 -2
- data/docs/prompt_management.md +11 -11
- data/docs/security.md +3 -3
- data/docs/workflows-and-pipelines.md +1 -1
- data/examples/README.md +6 -6
- data/examples/headlines +3 -3
- data/lib/aia/aia_completion.bash +2 -2
- data/lib/aia/aia_completion.fish +4 -4
- data/lib/aia/aia_completion.zsh +2 -2
- data/lib/aia/chat_processor_service.rb +31 -21
- data/lib/aia/config/cli_parser.rb +403 -403
- data/lib/aia/config/config_section.rb +87 -0
- data/lib/aia/config/defaults.yml +219 -0
- data/lib/aia/config/defaults_loader.rb +147 -0
- data/lib/aia/config/mcp_parser.rb +151 -0
- data/lib/aia/config/model_spec.rb +67 -0
- data/lib/aia/config/validator.rb +185 -136
- data/lib/aia/config.rb +336 -17
- data/lib/aia/directive_processor.rb +14 -6
- data/lib/aia/directives/configuration.rb +24 -10
- data/lib/aia/directives/models.rb +3 -4
- data/lib/aia/directives/utility.rb +3 -2
- data/lib/aia/directives/web_and_file.rb +50 -47
- data/lib/aia/logger.rb +328 -0
- data/lib/aia/prompt_handler.rb +18 -22
- data/lib/aia/ruby_llm_adapter.rb +572 -69
- data/lib/aia/session.rb +9 -8
- data/lib/aia/ui_presenter.rb +20 -16
- data/lib/aia/utility.rb +50 -18
- data/lib/aia.rb +91 -66
- data/lib/extensions/ruby_llm/modalities.rb +2 -0
- data/mcp_servers/apple-mcp.json +8 -0
- data/mcp_servers/mcp_server_chart.json +11 -0
- data/mcp_servers/playwright_one.json +8 -0
- data/mcp_servers/playwright_two.json +8 -0
- data/mcp_servers/tavily_mcp_server.json +8 -0
- metadata +83 -25
- data/lib/aia/config/base.rb +0 -308
- data/lib/aia/config/defaults.rb +0 -91
- data/lib/aia/config/file_loader.rb +0 -163
- data/mcp_servers/imcp.json +0 -7
- data/mcp_servers/launcher.json +0 -11
- data/mcp_servers/timeserver.json +0 -8
data/lib/aia/config/validator.rb
CHANGED
|
@@ -1,183 +1,232 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
41
|
+
def process_stdin_content
|
|
42
|
+
stdin_content = ''
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
53
|
+
stdin_content
|
|
54
|
+
end
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
def process_prompt_id_from_args(config, remaining_args)
|
|
57
|
+
return if remaining_args.empty?
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
76
|
+
config.context_files ||= []
|
|
77
|
+
config.context_files += remaining_args
|
|
78
|
+
end
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
def handle_executable_prompt(config)
|
|
81
|
+
return unless config.executable_prompt && config.context_files && !config.context_files.empty?
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
config.executable_prompt_file = config.context_files.pop
|
|
84
|
+
end
|
|
79
85
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
93
|
+
def process_role_configuration(config)
|
|
94
|
+
role = config.prompts.role
|
|
95
|
+
return if role.nil? || role.empty?
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
105
|
+
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix)
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
end
|
|
141
|
+
flags_section.send("#{flag}=", normalized)
|
|
142
|
+
end
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
dump_config(config, config.dump_file)
|
|
148
|
+
exit 0
|
|
149
|
+
end
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
def handle_completion_script(config)
|
|
152
|
+
return unless config.completion
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
generate_completion_script(config.completion)
|
|
155
|
+
exit
|
|
156
|
+
end
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
def generate_completion_script(shell)
|
|
159
|
+
script_path = File.join(File.dirname(__FILE__), "../../aia_completion.#{shell}")
|
|
159
160
|
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
-
|
|
177
|
+
def configure_prompt_manager(config)
|
|
178
|
+
return unless config.prompts.parameter_regex
|
|
167
179
|
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|