aia 1.1.1 → 2.0.0.0.pre.alpha
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/.envrc +5 -1
- data/.loki +231 -0
- data/.quality/flay_baseline.txt +1 -0
- data/.quality/flog_baseline.txt +29 -0
- data/.quality/reek_baseline.txt +80 -0
- data/.rubocop.yml +116 -0
- data/.version +1 -1
- data/CHANGELOG.md +259 -50
- data/IMPLEMENTATION_PLAN.md +506 -0
- data/README.md +266 -238
- data/Rakefile +118 -5
- data/architecture_review.md +314 -0
- data/bin/aia +16 -0
- data/docs/AGENTS.md +40 -0
- data/docs/advanced-prompting.md +67 -3
- data/docs/cli-reference.md +312 -56
- data/docs/configuration.md +130 -19
- data/docs/contributing.md +56 -2
- data/docs/directives-reference.md +593 -78
- data/docs/faq.md +85 -3
- data/docs/guides/available-models.md +1 -1
- data/docs/guides/basic-usage.md +6 -6
- data/docs/guides/chat.md +40 -16
- data/docs/guides/crew.md +239 -0
- data/docs/guides/executable-prompts.md +1 -1
- data/docs/guides/index.md +1 -0
- data/docs/guides/models.md +15 -0
- data/docs/index.md +29 -2
- data/docs/installation.md +44 -17
- data/docs/mcp-integration.md +40 -0
- data/docs/prompt_management.md +85 -86
- data/docs/security.md +47 -0
- data/docs/special_projects_guide.md +386 -0
- data/docs/tools-and-mcp-examples.md +23 -0
- data/docs/workflows-and-pipelines.md +84 -7
- data/examples/.gitignore +1 -0
- data/examples/00_setup_aia.sh +27 -44
- data/examples/11_multi_model.sh +4 -14
- data/examples/12_token_usage.sh +3 -12
- data/examples/18_tools.sh +10 -2
- data/examples/22_chat_mode.sh +0 -10
- data/examples/23_verify.sh +139 -0
- data/examples/24_decompose.sh +139 -0
- data/examples/25_spawn.sh +139 -0
- data/examples/26_debate.sh +97 -0
- data/examples/27_mention_routing.sh +157 -0
- data/examples/28_model_switching.sh +106 -0
- data/examples/29_agent_harness.sh +177 -0
- data/examples/README.md +65 -0
- data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
- data/examples/aia_config.yml +1 -1
- data/examples/aia_config_orchestrator.yml +45 -0
- data/examples/common.sh +18 -6
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/roles/orchestrator.md +21 -0
- data/examples/requirements/sinatra_taskflow_app.md +139 -0
- data/examples/rules/01_classify_ruby.rb +16 -0
- data/examples/rules/02_prefer_claude_for_code.rb +19 -0
- data/examples/rules/03_gate_prompt_length.rb +19 -0
- data/examples/rules/04_tool_selection.rb +41 -0
- data/examples/rules/README.md +30 -0
- data/examples/run_all.sh +48 -15
- data/examples/tools/word_count_tool.rb +1 -1
- data/lib/AGENTS.md +57 -0
- data/lib/aia/chat_loop.rb +304 -164
- data/lib/aia/config/cli_parser.rb +174 -111
- data/lib/aia/config/defaults.yml +62 -33
- data/lib/aia/config/mcp_parser.rb +39 -46
- data/lib/aia/config/model_spec.rb +34 -2
- data/lib/aia/config/validator.rb +108 -142
- data/lib/aia/config.rb +110 -145
- data/lib/aia/content_extractor.rb +153 -0
- data/lib/aia/cost_calculator.rb +38 -0
- data/lib/aia/crew.rb +164 -0
- data/lib/aia/debate_handler.rb +166 -0
- data/lib/aia/delegate_handler.rb +112 -0
- data/lib/aia/directive.rb +33 -18
- data/lib/aia/directive_processor.rb +16 -7
- data/lib/aia/directives/configuration_directives.rb +160 -20
- data/lib/aia/directives/context_directives.rb +38 -26
- data/lib/aia/directives/execution_directives.rb +136 -4
- data/lib/aia/directives/model_directives.rb +76 -34
- data/lib/aia/directives/trakflow_directives.rb +44 -0
- data/lib/aia/directives/utility_directives.rb +203 -6
- data/lib/aia/directives/web_and_file_directives.rb +96 -60
- data/lib/aia/errors.rb +15 -0
- data/lib/aia/fact_asserter.rb +27 -0
- data/lib/aia/fzf.rb +9 -31
- data/lib/aia/handler_context.rb +17 -0
- data/lib/aia/handler_protocol.rb +19 -0
- data/lib/aia/history_transfer.rb +55 -0
- data/lib/aia/input_collector.rb +3 -3
- data/lib/aia/layered_orchestrator.rb +448 -0
- data/lib/aia/logger.rb +24 -4
- data/lib/aia/mcp_config_normalizer.rb +35 -0
- data/lib/aia/mcp_connection_manager.rb +305 -0
- data/lib/aia/mcp_discovery.rb +44 -0
- data/lib/aia/mcp_grouper.rb +33 -0
- data/lib/aia/mcp_utility.rb +57 -0
- data/lib/aia/mention_router.rb +260 -0
- data/lib/aia/model_alias_registry.rb +97 -0
- data/lib/aia/model_switch_handler.rb +100 -0
- data/lib/aia/network_builder.rb +155 -0
- data/lib/aia/network_memory_manager.rb +55 -0
- data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
- data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
- data/lib/aia/pipeline_orchestrator.rb +262 -0
- data/lib/aia/plugin_loader.rb +170 -0
- data/lib/aia/plugin_monitor.rb +208 -0
- data/lib/aia/prompt_decomposer.rb +157 -0
- data/lib/aia/prompt_handler.rb +19 -39
- data/lib/aia/robot_builder.rb +51 -0
- data/lib/aia/robot_factory.rb +334 -0
- data/lib/aia/robot_namer.rb +116 -0
- data/lib/aia/session.rb +83 -17
- data/lib/aia/session_tracker.rb +209 -0
- data/lib/aia/similarity_scorer.rb +39 -0
- data/lib/aia/skill_utils.rb +105 -1
- data/lib/aia/spawn_handler.rb +129 -0
- data/lib/aia/spawn_spec_parser.rb +65 -0
- data/lib/aia/special_mode_handler.rb +302 -0
- data/lib/aia/startup_coordinator.rb +150 -0
- data/lib/aia/streaming_runner.rb +169 -0
- data/lib/aia/system_prompt_assembler.rb +88 -0
- data/lib/aia/task_coordinator.rb +202 -0
- data/lib/aia/task_decomposer.rb +57 -0
- data/lib/aia/task_executor.rb +51 -0
- data/lib/aia/tfidf_math.rb +27 -0
- data/lib/aia/tool_filter/tfidf.rb +116 -0
- data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
- data/lib/aia/tool_filter.rb +82 -0
- data/lib/aia/tool_filter_registry.rb +30 -0
- data/lib/aia/tool_filter_strategy.rb +143 -0
- data/lib/aia/tool_loader.rb +210 -0
- data/lib/aia/tool_utility.rb +30 -0
- data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
- data/lib/aia/tools/recruit_robot_tool.rb +60 -0
- data/lib/aia/tools/reskill_robot_tool.rb +44 -0
- data/lib/aia/tools/task_board_tool.rb +114 -0
- data/lib/aia/trakflow_bridge.rb +173 -0
- data/lib/aia/turn_state.rb +94 -0
- data/lib/aia/ui_presenter.rb +166 -198
- data/lib/aia/utility.rb +134 -87
- data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
- data/lib/aia/verification_network.rb +58 -0
- data/lib/aia.rb +108 -63
- data/mkdocs.yml +1 -0
- metadata +179 -56
- data/justfile +0 -215
- data/lib/aia/adapter/chat_execution.rb +0 -242
- data/lib/aia/adapter/error_handler.rb +0 -68
- data/lib/aia/adapter/gem_activator.rb +0 -57
- data/lib/aia/adapter/mcp_connector.rb +0 -274
- data/lib/aia/adapter/modality_handlers.rb +0 -167
- data/lib/aia/adapter/model_registry.rb +0 -81
- data/lib/aia/adapter/multi_model_chat.rb +0 -218
- data/lib/aia/adapter/provider_configurator.rb +0 -59
- data/lib/aia/adapter/tool_filter.rb +0 -85
- data/lib/aia/adapter/tool_loader.rb +0 -90
- data/lib/aia/chat_processor_service.rb +0 -178
- data/lib/aia/prompt_pipeline.rb +0 -183
- data/lib/aia/ruby_llm_adapter.rb +0 -95
- data/lib/extensions/openstruct_merge.rb +0 -48
- data/lib/extensions/ruby_llm/.irbrc +0 -56
- data/lib/extensions/ruby_llm/modalities.rb +0 -36
- data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
- data/lib/refinements/string.rb +0 -16
- data/main.just +0 -76
data/lib/aia/config/validator.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'word_wrapper'
|
|
4
|
-
require_relative '../adapter/gem_activator'
|
|
5
4
|
require_relative '../skill_utils'
|
|
6
5
|
|
|
7
6
|
# lib/aia/config/validator.rb
|
|
@@ -10,6 +9,7 @@ require_relative '../skill_utils'
|
|
|
10
9
|
# Handles prompt ID extraction, context file validation, role processing, etc.
|
|
11
10
|
|
|
12
11
|
module AIA
|
|
12
|
+
# rubocop:disable Metrics/ModuleLength
|
|
13
13
|
module ConfigValidator
|
|
14
14
|
class << self
|
|
15
15
|
# Tailor and validate the configuration
|
|
@@ -24,8 +24,7 @@ module AIA
|
|
|
24
24
|
stdin_content = process_stdin_content
|
|
25
25
|
config.stdin_content = stdin_content if stdin_content && !stdin_content.strip.empty?
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
# are registered into PM.directives before PromptHandler.new is called.
|
|
27
|
+
# Tool files may define AIA::Directive subclasses used by PromptManager.
|
|
29
28
|
require_tool_files(config)
|
|
30
29
|
|
|
31
30
|
# Process arguments and validate
|
|
@@ -33,30 +32,34 @@ module AIA
|
|
|
33
32
|
validate_and_set_context_files(config, remaining_args)
|
|
34
33
|
handle_executable_prompt(config)
|
|
35
34
|
handle_stdin_as_prompt(config)
|
|
36
|
-
|
|
37
|
-
handle_mcp_list(config)
|
|
38
|
-
handle_list_tools(config)
|
|
39
|
-
handle_list_skills(config)
|
|
40
|
-
handle_completion_script(config)
|
|
35
|
+
return :early_exit if handle_early_exits(config) == :early_exit
|
|
41
36
|
validate_required_prompt_id(config)
|
|
42
37
|
process_role_configuration(config)
|
|
38
|
+
validate_plugins_dir(config)
|
|
43
39
|
handle_fuzzy_search_prompt_id(config)
|
|
44
40
|
normalize_boolean_flags(config)
|
|
45
41
|
validate_final_prompt_requirements(config)
|
|
46
|
-
configure_prompt_manager(config)
|
|
47
42
|
prepare_pipeline(config)
|
|
48
43
|
validate_pipeline_prompts(config)
|
|
49
44
|
|
|
50
45
|
config
|
|
51
46
|
end
|
|
52
47
|
|
|
48
|
+
def handle_early_exits(config)
|
|
49
|
+
return :early_exit if handle_dump_config(config) == :early_exit
|
|
50
|
+
return :early_exit if handle_mcp_list(config) == :early_exit
|
|
51
|
+
return :early_exit if handle_list_tools(config) == :early_exit
|
|
52
|
+
return :early_exit if handle_list_skills(config) == :early_exit
|
|
53
|
+
:early_exit if handle_completion_script(config) == :early_exit
|
|
54
|
+
end
|
|
55
|
+
|
|
53
56
|
def process_stdin_content
|
|
54
57
|
stdin_content = String.new
|
|
55
58
|
|
|
56
|
-
if
|
|
59
|
+
if !$stdin.tty? && !$stdin.closed?
|
|
57
60
|
begin
|
|
58
|
-
stdin_content << "\n" +
|
|
59
|
-
|
|
61
|
+
stdin_content << ("\n" + $stdin.read)
|
|
62
|
+
$stdin.reopen('/dev/tty')
|
|
60
63
|
rescue => _
|
|
61
64
|
# If we can't reopen, continue without error
|
|
62
65
|
end
|
|
@@ -71,9 +74,8 @@ module AIA
|
|
|
71
74
|
maybe_id = remaining_args.first
|
|
72
75
|
maybe_id_plus = File.join(config.prompts.dir, maybe_id + config.prompts.extname)
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
end
|
|
77
|
+
return unless AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
|
|
78
|
+
config.prompt_id = remaining_args.shift
|
|
77
79
|
end
|
|
78
80
|
|
|
79
81
|
def validate_and_set_context_files(config, remaining_args)
|
|
@@ -81,8 +83,16 @@ module AIA
|
|
|
81
83
|
|
|
82
84
|
bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
|
|
83
85
|
if bad_files.any?
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
# Detect likely flag typos: "_B" instead of "-B", "_CD" instead of "-CD", etc.
|
|
87
|
+
flag_typos = bad_files.grep(/\A_[A-Za-z]/)
|
|
88
|
+
if flag_typos.any?
|
|
89
|
+
suggestions = flag_typos.map { |f| "-#{f[1..]}" }.join(', ')
|
|
90
|
+
raise AIA::ConfigurationError,
|
|
91
|
+
"Unknown argument(s): #{flag_typos.join(', ')}\n" \
|
|
92
|
+
"Did you mean: #{suggestions}?\n" \
|
|
93
|
+
"Flags require a dash prefix (e.g., -B not _B)."
|
|
94
|
+
end
|
|
95
|
+
raise AIA::ConfigurationError, "The following files do not exist: #{bad_files.join(', ')}"
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
config.context_files ||= []
|
|
@@ -90,7 +100,6 @@ module AIA
|
|
|
90
100
|
end
|
|
91
101
|
|
|
92
102
|
def handle_executable_prompt(config)
|
|
93
|
-
# Auto-detect: no prompt_id, first context_file starts with shebang
|
|
94
103
|
return unless config.prompt_id.nil?
|
|
95
104
|
return unless config.context_files && !config.context_files.empty?
|
|
96
105
|
|
|
@@ -100,7 +109,6 @@ module AIA
|
|
|
100
109
|
first_line = File.open(candidate, &:readline).strip rescue nil
|
|
101
110
|
return unless first_line&.start_with?('#!')
|
|
102
111
|
|
|
103
|
-
# This is an executable prompt — the file content IS the prompt
|
|
104
112
|
config.context_files.shift
|
|
105
113
|
config.executable_prompt_content = File.read(candidate).lines[1..].join
|
|
106
114
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
@@ -112,21 +120,20 @@ module AIA
|
|
|
112
120
|
|
|
113
121
|
content = config.stdin_content
|
|
114
122
|
|
|
115
|
-
# Strip shebang line if present (e.g., piped from an executable prompt)
|
|
116
123
|
if content.lines.first&.strip&.start_with?('#!')
|
|
117
124
|
content = content.lines[1..].join
|
|
118
125
|
end
|
|
119
126
|
|
|
120
127
|
config.executable_prompt_content = content
|
|
121
|
-
config.stdin_content = nil
|
|
128
|
+
config.stdin_content = nil
|
|
122
129
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
123
130
|
end
|
|
124
131
|
|
|
125
132
|
def validate_required_prompt_id(config)
|
|
126
|
-
return unless config.prompt_id.nil? &&
|
|
133
|
+
return unless config.prompt_id.nil? && config.flags.chat != true && config.flags.fuzzy != true
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
raise AIA::ConfigurationError,
|
|
136
|
+
"A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
|
130
137
|
end
|
|
131
138
|
|
|
132
139
|
def process_role_configuration(config)
|
|
@@ -134,7 +141,6 @@ module AIA
|
|
|
134
141
|
return if role.nil? || role.empty?
|
|
135
142
|
|
|
136
143
|
roles_prefix = config.prompts.roles_prefix
|
|
137
|
-
|
|
138
144
|
unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
|
|
139
145
|
config.prompts.role = "#{roles_prefix}/#{role}"
|
|
140
146
|
role = config.prompts.role
|
|
@@ -142,18 +148,21 @@ module AIA
|
|
|
142
148
|
|
|
143
149
|
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix.to_s)
|
|
144
150
|
|
|
145
|
-
# In chat-only mode (no prompt_id), leave the role configured so ChatLoop
|
|
146
|
-
# can inject it as initial context. Promoting it to prompt_id would cause
|
|
147
|
-
# PM to receive the role path as a literal string rather than file content.
|
|
148
151
|
return if config.flags&.chat == true
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
return unless config.prompt_id.nil? || config.prompt_id.empty?
|
|
154
|
+
return if role.nil? || role.empty?
|
|
155
|
+
config.prompt_id = role
|
|
156
|
+
config.pipeline.prepend(config.prompt_id)
|
|
157
|
+
config.prompts.role = ''
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def validate_plugins_dir(config)
|
|
161
|
+
plugins_dir = config.paths&.plugins_dir
|
|
162
|
+
return if plugins_dir.nil? || plugins_dir.to_s.strip.empty?
|
|
163
|
+
return if Dir.exist?(plugins_dir)
|
|
164
|
+
|
|
165
|
+
$stderr.puts "Warning: configured plugins directory does not exist: #{plugins_dir}"
|
|
157
166
|
end
|
|
158
167
|
|
|
159
168
|
def handle_fuzzy_search_prompt_id(config)
|
|
@@ -188,12 +197,12 @@ module AIA
|
|
|
188
197
|
return unless config.dump_file
|
|
189
198
|
|
|
190
199
|
dump_config(config, config.dump_file)
|
|
191
|
-
|
|
200
|
+
:early_exit
|
|
192
201
|
end
|
|
193
202
|
|
|
194
203
|
def handle_mcp_list(config)
|
|
195
204
|
return unless config.mcp_list
|
|
196
|
-
return if config.list_tools
|
|
205
|
+
return if config.list_tools
|
|
197
206
|
|
|
198
207
|
servers = filter_mcp_servers(config)
|
|
199
208
|
|
|
@@ -203,17 +212,18 @@ module AIA
|
|
|
203
212
|
label = mcp_filter_active?(config) ? "Active" : "Configured"
|
|
204
213
|
puts "#{label} MCP servers:\n\n"
|
|
205
214
|
servers.each do |server|
|
|
206
|
-
name
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
name = AIA::Utility.server_name(server) || '(unnamed)'
|
|
216
|
+
transport = server[:transport] || server['transport'] || {}
|
|
217
|
+
command = transport[:command] || transport['command'] || server[:command] || server['command'] || '(no command)'
|
|
218
|
+
args = transport[:args] || transport['args'] || server[:args] || server['args'] || []
|
|
219
|
+
args_str = args.empty? ? '' : " #{args.join(' ')}"
|
|
210
220
|
puts " #{name}"
|
|
211
221
|
puts " command: #{command}#{args_str}"
|
|
212
222
|
puts
|
|
213
223
|
end
|
|
214
224
|
end
|
|
215
225
|
|
|
216
|
-
|
|
226
|
+
:early_exit
|
|
217
227
|
end
|
|
218
228
|
|
|
219
229
|
def handle_list_tools(config)
|
|
@@ -228,7 +238,7 @@ module AIA
|
|
|
228
238
|
|
|
229
239
|
if local_tools.empty? && mcp_tool_groups.empty?
|
|
230
240
|
$stderr.puts "No tools available."
|
|
231
|
-
|
|
241
|
+
return :early_exit
|
|
232
242
|
end
|
|
233
243
|
|
|
234
244
|
if $stdout.tty?
|
|
@@ -237,41 +247,7 @@ module AIA
|
|
|
237
247
|
list_tools_markdown(local_tools, mcp_tool_groups)
|
|
238
248
|
end
|
|
239
249
|
|
|
240
|
-
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def handle_list_skills(config)
|
|
244
|
-
return unless config.list_skills
|
|
245
|
-
|
|
246
|
-
skills_dir = AIA.config.skills.dir
|
|
247
|
-
|
|
248
|
-
unless Dir.exist?(skills_dir)
|
|
249
|
-
$stderr.puts "No skills directory found at #{skills_dir}"
|
|
250
|
-
exit 0
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
skill_dirs = Dir.glob("*/SKILL.md", base: skills_dir).map { |f| File.dirname(f) }.sort
|
|
254
|
-
|
|
255
|
-
if skill_dirs.empty?
|
|
256
|
-
$stderr.puts "No skills found in #{skills_dir}"
|
|
257
|
-
exit 0
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
skill_dirs.each do |skill_name|
|
|
261
|
-
skill_md = File.join(skills_dir, skill_name, 'SKILL.md')
|
|
262
|
-
fm = AIA::SkillUtils.parse_front_matter(skill_md)
|
|
263
|
-
|
|
264
|
-
puts "## #{skill_name}"
|
|
265
|
-
puts
|
|
266
|
-
puts "| Key | Value |"
|
|
267
|
-
puts "|-----|-------|"
|
|
268
|
-
fm.each do |key, value|
|
|
269
|
-
puts "| #{key} | #{value} |"
|
|
270
|
-
end
|
|
271
|
-
puts
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
exit 0
|
|
250
|
+
:early_exit
|
|
275
251
|
end
|
|
276
252
|
|
|
277
253
|
def list_tools_terminal(local_tools, mcp_tool_groups)
|
|
@@ -330,25 +306,20 @@ module AIA
|
|
|
330
306
|
|
|
331
307
|
puts "### `#{name}`"
|
|
332
308
|
puts
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
end
|
|
309
|
+
return if desc.empty?
|
|
310
|
+
puts nest_markdown_headings(desc, 3)
|
|
311
|
+
puts
|
|
337
312
|
end
|
|
338
313
|
|
|
339
|
-
# Adjusts any markdown headings in text so they nest under the
|
|
340
|
-
# given parent heading level. e.g. with parent_level=3 (###),
|
|
341
|
-
# a "# Foo" becomes "#### Foo" and "## Bar" becomes "##### Bar".
|
|
342
|
-
# Handles headings with optional leading whitespace.
|
|
343
314
|
def nest_markdown_headings(text, parent_level)
|
|
344
|
-
text.gsub(/^[ \t]*(\#{1,6})\s/) do |
|
|
345
|
-
existing =
|
|
346
|
-
"#" * (existing.length + parent_level) + " "
|
|
315
|
+
text.gsub(/^[ \t]*(\#{1,6})\s/) do |_match|
|
|
316
|
+
existing = ::Regexp.last_match(1)
|
|
317
|
+
("#" * (existing.length + parent_level)) + " "
|
|
347
318
|
end
|
|
348
319
|
end
|
|
349
320
|
|
|
350
321
|
def filter_mcp_servers(config)
|
|
351
|
-
servers
|
|
322
|
+
servers = config.mcp_servers || []
|
|
352
323
|
use_list = Array(config.mcp_use)
|
|
353
324
|
skip_list = Array(config.mcp_skip)
|
|
354
325
|
|
|
@@ -371,26 +342,32 @@ module AIA
|
|
|
371
342
|
if File.exist?(expanded)
|
|
372
343
|
require expanded
|
|
373
344
|
else
|
|
374
|
-
|
|
345
|
+
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
375
346
|
end
|
|
376
347
|
rescue LoadError, StandardError => e
|
|
377
|
-
|
|
348
|
+
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
378
349
|
end
|
|
379
350
|
end
|
|
380
351
|
|
|
352
|
+
# Canonical --list-skills handler. Runs after config is built (so a -c
|
|
353
|
+
# config file's skills.dir is honored) and resolves skills the same way
|
|
354
|
+
# loading does, so what is listed is always loadable.
|
|
355
|
+
def handle_list_skills(config)
|
|
356
|
+
return unless config.respond_to?(:list_skills) && config.list_skills
|
|
357
|
+
|
|
358
|
+
puts AIA::SkillUtils.list_skills_markdown(config)
|
|
359
|
+
:early_exit
|
|
360
|
+
end
|
|
361
|
+
|
|
381
362
|
def load_local_tools(config)
|
|
382
|
-
# Load required libraries
|
|
363
|
+
# Load required libraries
|
|
383
364
|
Array(config.require_libs).each do |lib|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
warn "Hint: Make sure the gem is installed: gem install #{lib}"
|
|
391
|
-
rescue StandardError => e
|
|
392
|
-
warn "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
|
|
393
|
-
end
|
|
365
|
+
require lib
|
|
366
|
+
rescue LoadError => e
|
|
367
|
+
$stderr.puts "Warning: Failed to require '#{lib}': #{e.message}"
|
|
368
|
+
$stderr.puts "Hint: Make sure the gem is installed: gem install #{lib}"
|
|
369
|
+
rescue StandardError => e
|
|
370
|
+
$stderr.puts "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
|
|
394
371
|
end
|
|
395
372
|
|
|
396
373
|
# Load tool files
|
|
@@ -399,48 +376,49 @@ module AIA
|
|
|
399
376
|
if File.exist?(expanded)
|
|
400
377
|
require expanded
|
|
401
378
|
else
|
|
402
|
-
|
|
379
|
+
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
403
380
|
end
|
|
404
381
|
rescue LoadError, StandardError => e
|
|
405
|
-
|
|
382
|
+
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
406
383
|
end
|
|
407
384
|
|
|
408
385
|
# Scan ObjectSpace for RubyLLM::Tool subclasses
|
|
409
386
|
ObjectSpace.each_object(Class).select do |klass|
|
|
410
|
-
next false unless klass < RubyLLM::Tool
|
|
387
|
+
next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
|
|
411
388
|
|
|
412
389
|
begin
|
|
413
390
|
klass.new
|
|
414
391
|
true
|
|
415
|
-
rescue
|
|
392
|
+
rescue StandardError
|
|
416
393
|
false
|
|
417
394
|
end
|
|
418
395
|
end
|
|
419
396
|
end
|
|
420
397
|
|
|
421
398
|
def first_sentences(text, count)
|
|
422
|
-
# Normalize whitespace: collapse newlines and multiple spaces
|
|
423
399
|
normalized = text.gsub(/\s*\n\s*/, ' ').gsub(/\s{2,}/, ' ').strip
|
|
424
400
|
sentences = normalized.scan(/[^.!?]*[.!?]/)
|
|
425
401
|
result = sentences.first(count).join.strip
|
|
426
402
|
result.empty? ? normalized : result
|
|
427
403
|
end
|
|
428
404
|
|
|
405
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
429
406
|
def load_mcp_tools_grouped(config)
|
|
430
407
|
servers = filter_mcp_servers(config)
|
|
431
408
|
return {} if servers.empty?
|
|
432
409
|
|
|
433
|
-
# Suppress MCP logger noise during listing
|
|
434
410
|
quiet_mcp_logger
|
|
435
411
|
|
|
436
412
|
groups = {}
|
|
437
413
|
default_timeout = 8_000
|
|
438
414
|
|
|
415
|
+
# rubocop:disable Metrics/BlockLength
|
|
439
416
|
servers.each do |server|
|
|
440
|
-
name
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
417
|
+
name = AIA::Utility.server_name(server)
|
|
418
|
+
transport = server[:transport] || server['transport'] || {}
|
|
419
|
+
command = transport[:command] || transport['command'] || server[:command] || server['command']
|
|
420
|
+
args = transport[:args] || transport['args'] || server[:args] || server['args'] || []
|
|
421
|
+
env = transport[:env] || transport['env'] || server[:env] || server['env'] || {}
|
|
444
422
|
|
|
445
423
|
raw_timeout = server[:timeout] || server['timeout'] || default_timeout
|
|
446
424
|
timeout = raw_timeout.to_i < 1000 ? (raw_timeout.to_i * 1000) : raw_timeout.to_i
|
|
@@ -478,26 +456,27 @@ module AIA
|
|
|
478
456
|
rescue StandardError => e
|
|
479
457
|
$stderr.puts " error: #{e.message}"
|
|
480
458
|
end
|
|
459
|
+
# rubocop:enable Metrics/BlockLength
|
|
481
460
|
end
|
|
461
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
482
462
|
|
|
483
463
|
groups
|
|
484
464
|
end
|
|
465
|
+
# rubocop:enable Metrics/ModuleLength
|
|
485
466
|
|
|
486
467
|
def quiet_mcp_logger
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
end
|
|
493
|
-
end
|
|
468
|
+
return unless defined?(RubyLLM::MCP) && RubyLLM::MCP.respond_to?(:config)
|
|
469
|
+
mcp_config = RubyLLM::MCP.config
|
|
470
|
+
return unless mcp_config.respond_to?(:logger=)
|
|
471
|
+
quiet = Logger.new(File::NULL)
|
|
472
|
+
mcp_config.logger = quiet
|
|
494
473
|
end
|
|
495
474
|
|
|
496
475
|
def handle_completion_script(config)
|
|
497
476
|
return unless config.completion
|
|
498
477
|
|
|
499
478
|
generate_completion_script(config.completion)
|
|
500
|
-
|
|
479
|
+
:early_exit
|
|
501
480
|
end
|
|
502
481
|
|
|
503
482
|
def generate_completion_script(shell)
|
|
@@ -506,25 +485,18 @@ module AIA
|
|
|
506
485
|
if File.exist?(script_path)
|
|
507
486
|
puts File.read(script_path)
|
|
508
487
|
else
|
|
509
|
-
|
|
488
|
+
$stderr.puts "ERROR: The shell '#{shell}' is not supported or the completion script is missing."
|
|
510
489
|
end
|
|
511
490
|
end
|
|
512
491
|
|
|
513
492
|
def validate_final_prompt_requirements(config)
|
|
514
493
|
chat_mode = config.flags.chat == true
|
|
515
494
|
fuzzy_mode = config.flags.fuzzy == true
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def configure_prompt_manager(config)
|
|
523
|
-
# PM v1.0.0 uses ERB parameters (<%= param %>) instead of regex-based extraction.
|
|
524
|
-
# parameter_regex is deprecated and ignored.
|
|
525
|
-
if config.prompts.parameter_regex
|
|
526
|
-
warn "Warning: --regex / parameter_regex is deprecated. PM v1.0.0 uses ERB parameters (<%= param %>)."
|
|
527
|
-
end
|
|
495
|
+
no_prompt = config.prompt_id.nil? || config.prompt_id.empty?
|
|
496
|
+
no_context = config.context_files.nil? || config.context_files.empty?
|
|
497
|
+
return unless !chat_mode && !fuzzy_mode && no_prompt && no_context
|
|
498
|
+
raise AIA::ConfigurationError,
|
|
499
|
+
"A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
|
528
500
|
end
|
|
529
501
|
|
|
530
502
|
def prepare_pipeline(config)
|
|
@@ -543,24 +515,19 @@ module AIA
|
|
|
543
515
|
|
|
544
516
|
prompt_file_path = File.join(config.prompts.dir, "#{prompt_id}#{config.prompts.extname}")
|
|
545
517
|
unless File.exist?(prompt_file_path)
|
|
546
|
-
|
|
518
|
+
$stderr.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
|
|
547
519
|
and_exit = true
|
|
548
520
|
end
|
|
549
521
|
end
|
|
550
522
|
|
|
551
|
-
|
|
523
|
+
raise AIA::ConfigurationError, "One or more prompt IDs do not exist." if and_exit
|
|
552
524
|
end
|
|
553
525
|
|
|
554
|
-
# Dump configuration to file
|
|
555
|
-
#
|
|
556
|
-
# @param config [AIA::Config] the configuration to dump
|
|
557
|
-
# @param file [String] the file path to dump to
|
|
558
526
|
def dump_config(config, file)
|
|
559
527
|
ext = File.extname(file).downcase
|
|
560
528
|
|
|
561
529
|
config_hash = config.to_h
|
|
562
530
|
|
|
563
|
-
# Remove runtime keys
|
|
564
531
|
config_hash.delete(:prompt_id)
|
|
565
532
|
config_hash.delete(:dump_file)
|
|
566
533
|
|
|
@@ -575,7 +542,6 @@ module AIA
|
|
|
575
542
|
File.write(file, content)
|
|
576
543
|
puts "Config successfully dumped to #{file}"
|
|
577
544
|
end
|
|
578
|
-
|
|
579
545
|
end
|
|
580
546
|
end
|
|
581
547
|
end
|