aia 1.1.0 → 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 +266 -42
- 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 +19 -0
- data/examples/context/tech_stack.md +2 -2
- data/examples/prompts_dir/project_summary +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 +306 -159
- 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 +121 -138
- 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 -164
- 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,35 +24,42 @@ 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
|
+
# Tool files may define AIA::Directive subclasses used by PromptManager.
|
|
28
|
+
require_tool_files(config)
|
|
29
|
+
|
|
27
30
|
# Process arguments and validate
|
|
28
31
|
process_prompt_id_from_args(config, remaining_args)
|
|
29
32
|
validate_and_set_context_files(config, remaining_args)
|
|
30
33
|
handle_executable_prompt(config)
|
|
31
34
|
handle_stdin_as_prompt(config)
|
|
32
|
-
|
|
33
|
-
handle_mcp_list(config)
|
|
34
|
-
handle_list_tools(config)
|
|
35
|
-
handle_list_skills(config)
|
|
36
|
-
handle_completion_script(config)
|
|
35
|
+
return :early_exit if handle_early_exits(config) == :early_exit
|
|
37
36
|
validate_required_prompt_id(config)
|
|
38
37
|
process_role_configuration(config)
|
|
38
|
+
validate_plugins_dir(config)
|
|
39
39
|
handle_fuzzy_search_prompt_id(config)
|
|
40
40
|
normalize_boolean_flags(config)
|
|
41
41
|
validate_final_prompt_requirements(config)
|
|
42
|
-
configure_prompt_manager(config)
|
|
43
42
|
prepare_pipeline(config)
|
|
44
43
|
validate_pipeline_prompts(config)
|
|
45
44
|
|
|
46
45
|
config
|
|
47
46
|
end
|
|
48
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
|
+
|
|
49
56
|
def process_stdin_content
|
|
50
57
|
stdin_content = String.new
|
|
51
58
|
|
|
52
|
-
if
|
|
59
|
+
if !$stdin.tty? && !$stdin.closed?
|
|
53
60
|
begin
|
|
54
|
-
stdin_content << "\n" +
|
|
55
|
-
|
|
61
|
+
stdin_content << ("\n" + $stdin.read)
|
|
62
|
+
$stdin.reopen('/dev/tty')
|
|
56
63
|
rescue => _
|
|
57
64
|
# If we can't reopen, continue without error
|
|
58
65
|
end
|
|
@@ -67,9 +74,8 @@ module AIA
|
|
|
67
74
|
maybe_id = remaining_args.first
|
|
68
75
|
maybe_id_plus = File.join(config.prompts.dir, maybe_id + config.prompts.extname)
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
end
|
|
77
|
+
return unless AIA.bad_file?(maybe_id) && AIA.good_file?(maybe_id_plus)
|
|
78
|
+
config.prompt_id = remaining_args.shift
|
|
73
79
|
end
|
|
74
80
|
|
|
75
81
|
def validate_and_set_context_files(config, remaining_args)
|
|
@@ -77,8 +83,16 @@ module AIA
|
|
|
77
83
|
|
|
78
84
|
bad_files = remaining_args.reject { |filename| AIA.good_file?(filename) }
|
|
79
85
|
if bad_files.any?
|
|
80
|
-
|
|
81
|
-
|
|
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(', ')}"
|
|
82
96
|
end
|
|
83
97
|
|
|
84
98
|
config.context_files ||= []
|
|
@@ -86,7 +100,6 @@ module AIA
|
|
|
86
100
|
end
|
|
87
101
|
|
|
88
102
|
def handle_executable_prompt(config)
|
|
89
|
-
# Auto-detect: no prompt_id, first context_file starts with shebang
|
|
90
103
|
return unless config.prompt_id.nil?
|
|
91
104
|
return unless config.context_files && !config.context_files.empty?
|
|
92
105
|
|
|
@@ -96,7 +109,6 @@ module AIA
|
|
|
96
109
|
first_line = File.open(candidate, &:readline).strip rescue nil
|
|
97
110
|
return unless first_line&.start_with?('#!')
|
|
98
111
|
|
|
99
|
-
# This is an executable prompt — the file content IS the prompt
|
|
100
112
|
config.context_files.shift
|
|
101
113
|
config.executable_prompt_content = File.read(candidate).lines[1..].join
|
|
102
114
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
@@ -108,21 +120,20 @@ module AIA
|
|
|
108
120
|
|
|
109
121
|
content = config.stdin_content
|
|
110
122
|
|
|
111
|
-
# Strip shebang line if present (e.g., piped from an executable prompt)
|
|
112
123
|
if content.lines.first&.strip&.start_with?('#!')
|
|
113
124
|
content = content.lines[1..].join
|
|
114
125
|
end
|
|
115
126
|
|
|
116
127
|
config.executable_prompt_content = content
|
|
117
|
-
config.stdin_content = nil
|
|
128
|
+
config.stdin_content = nil
|
|
118
129
|
config.prompt_id = '__EXECUTABLE_PROMPT__'
|
|
119
130
|
end
|
|
120
131
|
|
|
121
132
|
def validate_required_prompt_id(config)
|
|
122
|
-
return unless config.prompt_id.nil? &&
|
|
133
|
+
return unless config.prompt_id.nil? && config.flags.chat != true && config.flags.fuzzy != true
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
135
|
+
raise AIA::ConfigurationError,
|
|
136
|
+
"A prompt ID is required unless using --chat, --fuzzy, or providing context files. Use -h or --help for help."
|
|
126
137
|
end
|
|
127
138
|
|
|
128
139
|
def process_role_configuration(config)
|
|
@@ -130,7 +141,6 @@ module AIA
|
|
|
130
141
|
return if role.nil? || role.empty?
|
|
131
142
|
|
|
132
143
|
roles_prefix = config.prompts.roles_prefix
|
|
133
|
-
|
|
134
144
|
unless AIA::SkillUtils.path_based_id?(role) || roles_prefix.nil? || roles_prefix.empty? || role.start_with?(roles_prefix)
|
|
135
145
|
config.prompts.role = "#{roles_prefix}/#{role}"
|
|
136
146
|
role = config.prompts.role
|
|
@@ -138,18 +148,21 @@ module AIA
|
|
|
138
148
|
|
|
139
149
|
config.prompts.roles_dir ||= File.join(config.prompts.dir, roles_prefix.to_s)
|
|
140
150
|
|
|
141
|
-
# In chat-only mode (no prompt_id), leave the role configured so ChatLoop
|
|
142
|
-
# can inject it as initial context. Promoting it to prompt_id would cause
|
|
143
|
-
# PM to receive the role path as a literal string rather than file content.
|
|
144
151
|
return if config.flags&.chat == true
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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}"
|
|
153
166
|
end
|
|
154
167
|
|
|
155
168
|
def handle_fuzzy_search_prompt_id(config)
|
|
@@ -184,12 +197,12 @@ module AIA
|
|
|
184
197
|
return unless config.dump_file
|
|
185
198
|
|
|
186
199
|
dump_config(config, config.dump_file)
|
|
187
|
-
|
|
200
|
+
:early_exit
|
|
188
201
|
end
|
|
189
202
|
|
|
190
203
|
def handle_mcp_list(config)
|
|
191
204
|
return unless config.mcp_list
|
|
192
|
-
return if config.list_tools
|
|
205
|
+
return if config.list_tools
|
|
193
206
|
|
|
194
207
|
servers = filter_mcp_servers(config)
|
|
195
208
|
|
|
@@ -199,17 +212,18 @@ module AIA
|
|
|
199
212
|
label = mcp_filter_active?(config) ? "Active" : "Configured"
|
|
200
213
|
puts "#{label} MCP servers:\n\n"
|
|
201
214
|
servers.each do |server|
|
|
202
|
-
name
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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(' ')}"
|
|
206
220
|
puts " #{name}"
|
|
207
221
|
puts " command: #{command}#{args_str}"
|
|
208
222
|
puts
|
|
209
223
|
end
|
|
210
224
|
end
|
|
211
225
|
|
|
212
|
-
|
|
226
|
+
:early_exit
|
|
213
227
|
end
|
|
214
228
|
|
|
215
229
|
def handle_list_tools(config)
|
|
@@ -224,7 +238,7 @@ module AIA
|
|
|
224
238
|
|
|
225
239
|
if local_tools.empty? && mcp_tool_groups.empty?
|
|
226
240
|
$stderr.puts "No tools available."
|
|
227
|
-
|
|
241
|
+
return :early_exit
|
|
228
242
|
end
|
|
229
243
|
|
|
230
244
|
if $stdout.tty?
|
|
@@ -233,41 +247,7 @@ module AIA
|
|
|
233
247
|
list_tools_markdown(local_tools, mcp_tool_groups)
|
|
234
248
|
end
|
|
235
249
|
|
|
236
|
-
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def handle_list_skills(config)
|
|
240
|
-
return unless config.list_skills
|
|
241
|
-
|
|
242
|
-
skills_dir = AIA.config.skills.dir
|
|
243
|
-
|
|
244
|
-
unless Dir.exist?(skills_dir)
|
|
245
|
-
$stderr.puts "No skills directory found at #{skills_dir}"
|
|
246
|
-
exit 0
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
skill_dirs = Dir.glob("*/SKILL.md", base: skills_dir).map { |f| File.dirname(f) }.sort
|
|
250
|
-
|
|
251
|
-
if skill_dirs.empty?
|
|
252
|
-
$stderr.puts "No skills found in #{skills_dir}"
|
|
253
|
-
exit 0
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
skill_dirs.each do |skill_name|
|
|
257
|
-
skill_md = File.join(skills_dir, skill_name, 'SKILL.md')
|
|
258
|
-
fm = AIA::SkillUtils.parse_front_matter(skill_md)
|
|
259
|
-
|
|
260
|
-
puts "## #{skill_name}"
|
|
261
|
-
puts
|
|
262
|
-
puts "| Key | Value |"
|
|
263
|
-
puts "|-----|-------|"
|
|
264
|
-
fm.each do |key, value|
|
|
265
|
-
puts "| #{key} | #{value} |"
|
|
266
|
-
end
|
|
267
|
-
puts
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
exit 0
|
|
250
|
+
:early_exit
|
|
271
251
|
end
|
|
272
252
|
|
|
273
253
|
def list_tools_terminal(local_tools, mcp_tool_groups)
|
|
@@ -326,25 +306,20 @@ module AIA
|
|
|
326
306
|
|
|
327
307
|
puts "### `#{name}`"
|
|
328
308
|
puts
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
end
|
|
309
|
+
return if desc.empty?
|
|
310
|
+
puts nest_markdown_headings(desc, 3)
|
|
311
|
+
puts
|
|
333
312
|
end
|
|
334
313
|
|
|
335
|
-
# Adjusts any markdown headings in text so they nest under the
|
|
336
|
-
# given parent heading level. e.g. with parent_level=3 (###),
|
|
337
|
-
# a "# Foo" becomes "#### Foo" and "## Bar" becomes "##### Bar".
|
|
338
|
-
# Handles headings with optional leading whitespace.
|
|
339
314
|
def nest_markdown_headings(text, parent_level)
|
|
340
|
-
text.gsub(/^[ \t]*(\#{1,6})\s/) do |
|
|
341
|
-
existing =
|
|
342
|
-
"#" * (existing.length + parent_level) + " "
|
|
315
|
+
text.gsub(/^[ \t]*(\#{1,6})\s/) do |_match|
|
|
316
|
+
existing = ::Regexp.last_match(1)
|
|
317
|
+
("#" * (existing.length + parent_level)) + " "
|
|
343
318
|
end
|
|
344
319
|
end
|
|
345
320
|
|
|
346
321
|
def filter_mcp_servers(config)
|
|
347
|
-
servers
|
|
322
|
+
servers = config.mcp_servers || []
|
|
348
323
|
use_list = Array(config.mcp_use)
|
|
349
324
|
skip_list = Array(config.mcp_skip)
|
|
350
325
|
|
|
@@ -361,19 +336,38 @@ module AIA
|
|
|
361
336
|
!Array(config.mcp_use).empty? || !Array(config.mcp_skip).empty?
|
|
362
337
|
end
|
|
363
338
|
|
|
339
|
+
def require_tool_files(config)
|
|
340
|
+
Array(config.tools&.paths).each do |path|
|
|
341
|
+
expanded = File.expand_path(path)
|
|
342
|
+
if File.exist?(expanded)
|
|
343
|
+
require expanded
|
|
344
|
+
else
|
|
345
|
+
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
346
|
+
end
|
|
347
|
+
rescue LoadError, StandardError => e
|
|
348
|
+
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
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
|
+
|
|
364
362
|
def load_local_tools(config)
|
|
365
|
-
# Load required libraries
|
|
363
|
+
# Load required libraries
|
|
366
364
|
Array(config.require_libs).each do |lib|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
warn "Hint: Make sure the gem is installed: gem install #{lib}"
|
|
374
|
-
rescue StandardError => e
|
|
375
|
-
warn "Warning: Error in library '#{lib}': #{e.class} - #{e.message}"
|
|
376
|
-
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}"
|
|
377
371
|
end
|
|
378
372
|
|
|
379
373
|
# Load tool files
|
|
@@ -382,48 +376,49 @@ module AIA
|
|
|
382
376
|
if File.exist?(expanded)
|
|
383
377
|
require expanded
|
|
384
378
|
else
|
|
385
|
-
|
|
379
|
+
$stderr.puts "Warning: Tool file not found: #{path}"
|
|
386
380
|
end
|
|
387
381
|
rescue LoadError, StandardError => e
|
|
388
|
-
|
|
382
|
+
$stderr.puts "Warning: Failed to load tool '#{path}': #{e.message}"
|
|
389
383
|
end
|
|
390
384
|
|
|
391
385
|
# Scan ObjectSpace for RubyLLM::Tool subclasses
|
|
392
386
|
ObjectSpace.each_object(Class).select do |klass|
|
|
393
|
-
next false unless klass < RubyLLM::Tool
|
|
387
|
+
next false unless defined?(RubyLLM::Tool) && klass < RubyLLM::Tool
|
|
394
388
|
|
|
395
389
|
begin
|
|
396
390
|
klass.new
|
|
397
391
|
true
|
|
398
|
-
rescue
|
|
392
|
+
rescue StandardError
|
|
399
393
|
false
|
|
400
394
|
end
|
|
401
395
|
end
|
|
402
396
|
end
|
|
403
397
|
|
|
404
398
|
def first_sentences(text, count)
|
|
405
|
-
# Normalize whitespace: collapse newlines and multiple spaces
|
|
406
399
|
normalized = text.gsub(/\s*\n\s*/, ' ').gsub(/\s{2,}/, ' ').strip
|
|
407
400
|
sentences = normalized.scan(/[^.!?]*[.!?]/)
|
|
408
401
|
result = sentences.first(count).join.strip
|
|
409
402
|
result.empty? ? normalized : result
|
|
410
403
|
end
|
|
411
404
|
|
|
405
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
412
406
|
def load_mcp_tools_grouped(config)
|
|
413
407
|
servers = filter_mcp_servers(config)
|
|
414
408
|
return {} if servers.empty?
|
|
415
409
|
|
|
416
|
-
# Suppress MCP logger noise during listing
|
|
417
410
|
quiet_mcp_logger
|
|
418
411
|
|
|
419
412
|
groups = {}
|
|
420
413
|
default_timeout = 8_000
|
|
421
414
|
|
|
415
|
+
# rubocop:disable Metrics/BlockLength
|
|
422
416
|
servers.each do |server|
|
|
423
|
-
name
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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'] || {}
|
|
427
422
|
|
|
428
423
|
raw_timeout = server[:timeout] || server['timeout'] || default_timeout
|
|
429
424
|
timeout = raw_timeout.to_i < 1000 ? (raw_timeout.to_i * 1000) : raw_timeout.to_i
|
|
@@ -461,26 +456,27 @@ module AIA
|
|
|
461
456
|
rescue StandardError => e
|
|
462
457
|
$stderr.puts " error: #{e.message}"
|
|
463
458
|
end
|
|
459
|
+
# rubocop:enable Metrics/BlockLength
|
|
464
460
|
end
|
|
461
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
465
462
|
|
|
466
463
|
groups
|
|
467
464
|
end
|
|
465
|
+
# rubocop:enable Metrics/ModuleLength
|
|
468
466
|
|
|
469
467
|
def quiet_mcp_logger
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
end
|
|
476
|
-
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
|
|
477
473
|
end
|
|
478
474
|
|
|
479
475
|
def handle_completion_script(config)
|
|
480
476
|
return unless config.completion
|
|
481
477
|
|
|
482
478
|
generate_completion_script(config.completion)
|
|
483
|
-
|
|
479
|
+
:early_exit
|
|
484
480
|
end
|
|
485
481
|
|
|
486
482
|
def generate_completion_script(shell)
|
|
@@ -489,25 +485,18 @@ module AIA
|
|
|
489
485
|
if File.exist?(script_path)
|
|
490
486
|
puts File.read(script_path)
|
|
491
487
|
else
|
|
492
|
-
|
|
488
|
+
$stderr.puts "ERROR: The shell '#{shell}' is not supported or the completion script is missing."
|
|
493
489
|
end
|
|
494
490
|
end
|
|
495
491
|
|
|
496
492
|
def validate_final_prompt_requirements(config)
|
|
497
493
|
chat_mode = config.flags.chat == true
|
|
498
494
|
fuzzy_mode = config.flags.fuzzy == true
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def configure_prompt_manager(config)
|
|
506
|
-
# PM v1.0.0 uses ERB parameters (<%= param %>) instead of regex-based extraction.
|
|
507
|
-
# parameter_regex is deprecated and ignored.
|
|
508
|
-
if config.prompts.parameter_regex
|
|
509
|
-
warn "Warning: --regex / parameter_regex is deprecated. PM v1.0.0 uses ERB parameters (<%= param %>)."
|
|
510
|
-
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."
|
|
511
500
|
end
|
|
512
501
|
|
|
513
502
|
def prepare_pipeline(config)
|
|
@@ -526,24 +515,19 @@ module AIA
|
|
|
526
515
|
|
|
527
516
|
prompt_file_path = File.join(config.prompts.dir, "#{prompt_id}#{config.prompts.extname}")
|
|
528
517
|
unless File.exist?(prompt_file_path)
|
|
529
|
-
|
|
518
|
+
$stderr.puts "Error: Prompt ID '#{prompt_id}' does not exist at #{prompt_file_path}"
|
|
530
519
|
and_exit = true
|
|
531
520
|
end
|
|
532
521
|
end
|
|
533
522
|
|
|
534
|
-
|
|
523
|
+
raise AIA::ConfigurationError, "One or more prompt IDs do not exist." if and_exit
|
|
535
524
|
end
|
|
536
525
|
|
|
537
|
-
# Dump configuration to file
|
|
538
|
-
#
|
|
539
|
-
# @param config [AIA::Config] the configuration to dump
|
|
540
|
-
# @param file [String] the file path to dump to
|
|
541
526
|
def dump_config(config, file)
|
|
542
527
|
ext = File.extname(file).downcase
|
|
543
528
|
|
|
544
529
|
config_hash = config.to_h
|
|
545
530
|
|
|
546
|
-
# Remove runtime keys
|
|
547
531
|
config_hash.delete(:prompt_id)
|
|
548
532
|
config_hash.delete(:dump_file)
|
|
549
533
|
|
|
@@ -558,7 +542,6 @@ module AIA
|
|
|
558
542
|
File.write(file, content)
|
|
559
543
|
puts "Config successfully dumped to #{file}"
|
|
560
544
|
end
|
|
561
|
-
|
|
562
545
|
end
|
|
563
546
|
end
|
|
564
547
|
end
|