aia 1.1.1 → 2.0.0.0.pre.beta2

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +9 -1
  3. data/.loki +11 -0
  4. data/.reek.yml +160 -0
  5. data/.rubocop.yml +116 -0
  6. data/.rubocop_strict.yml +15 -0
  7. data/.version +1 -1
  8. data/CHANGELOG.md +269 -55
  9. data/IMPLEMENTATION_PLAN.md +506 -0
  10. data/README.md +267 -239
  11. data/Rakefile +5 -5
  12. data/_typos.toml +10 -0
  13. data/aia.gemspec +92 -0
  14. data/architecture_review.md +314 -0
  15. data/bin/aia +16 -0
  16. data/config/aia.yml +13 -0
  17. data/docs/AGENTS.md +40 -0
  18. data/docs/advanced-prompting.md +67 -3
  19. data/docs/cli-reference.md +312 -56
  20. data/docs/configuration.md +130 -19
  21. data/docs/contributing.md +56 -2
  22. data/docs/directives-reference.md +593 -78
  23. data/docs/faq.md +85 -3
  24. data/docs/guides/available-models.md +1 -1
  25. data/docs/guides/basic-usage.md +6 -6
  26. data/docs/guides/chat.md +40 -16
  27. data/docs/guides/crew.md +239 -0
  28. data/docs/guides/executable-prompts.md +1 -1
  29. data/docs/guides/index.md +1 -0
  30. data/docs/guides/models.md +15 -0
  31. data/docs/index.md +29 -2
  32. data/docs/installation.md +44 -17
  33. data/docs/mcp-integration.md +40 -0
  34. data/docs/prompt_management.md +85 -86
  35. data/docs/security.md +47 -0
  36. data/docs/special_projects_guide.md +386 -0
  37. data/docs/tools-and-mcp-examples.md +23 -0
  38. data/docs/workflows-and-pipelines.md +84 -7
  39. data/examples/.gitignore +1 -0
  40. data/examples/00_setup_aia.sh +27 -44
  41. data/examples/11_multi_model.sh +4 -14
  42. data/examples/12_token_usage.sh +3 -12
  43. data/examples/18_tools.sh +10 -2
  44. data/examples/22_chat_mode.sh +0 -10
  45. data/examples/23_verify.sh +139 -0
  46. data/examples/24_decompose.sh +139 -0
  47. data/examples/25_spawn.sh +139 -0
  48. data/examples/26_debate.sh +97 -0
  49. data/examples/27_mention_routing.sh +157 -0
  50. data/examples/28_model_switching.sh +106 -0
  51. data/examples/29_agent_harness.sh +177 -0
  52. data/examples/README.md +65 -0
  53. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  54. data/examples/aia_config.yml +1 -1
  55. data/examples/aia_config_orchestrator.yml +45 -0
  56. data/examples/common.sh +18 -6
  57. data/examples/context/tech_stack.md +2 -2
  58. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  59. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  60. data/examples/rules/01_classify_ruby.rb +16 -0
  61. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  62. data/examples/rules/03_gate_prompt_length.rb +19 -0
  63. data/examples/rules/04_tool_selection.rb +41 -0
  64. data/examples/rules/README.md +30 -0
  65. data/examples/run_all.sh +48 -15
  66. data/examples/tools/word_count_tool.rb +1 -1
  67. data/lib/AGENTS.md +57 -0
  68. data/lib/aia/chat_loop.rb +263 -167
  69. data/lib/aia/config/cli_parser.rb +217 -145
  70. data/lib/aia/config/defaults.yml +62 -33
  71. data/lib/aia/config/mcp_parser.rb +52 -51
  72. data/lib/aia/config/model_spec.rb +34 -2
  73. data/lib/aia/config/validator.rb +171 -216
  74. data/lib/aia/config.rb +111 -145
  75. data/lib/aia/content_extractor.rb +155 -0
  76. data/lib/aia/cost_calculator.rb +39 -0
  77. data/lib/aia/crew.rb +164 -0
  78. data/lib/aia/debate_handler.rb +174 -0
  79. data/lib/aia/delegate_handler.rb +116 -0
  80. data/lib/aia/directive.rb +43 -26
  81. data/lib/aia/directive_processor.rb +16 -7
  82. data/lib/aia/directives/configuration_directives.rb +214 -60
  83. data/lib/aia/directives/context_directives.rb +67 -52
  84. data/lib/aia/directives/execution_directives.rb +141 -4
  85. data/lib/aia/directives/model_directives.rb +163 -141
  86. data/lib/aia/directives/trakflow_directives.rb +62 -0
  87. data/lib/aia/directives/utility_directives.rb +227 -30
  88. data/lib/aia/directives/web_and_file_directives.rb +126 -77
  89. data/lib/aia/errors.rb +15 -0
  90. data/lib/aia/fact_asserter.rb +27 -0
  91. data/lib/aia/fzf.rb +9 -31
  92. data/lib/aia/handler_context.rb +17 -0
  93. data/lib/aia/handler_protocol.rb +19 -0
  94. data/lib/aia/history_transfer.rb +55 -0
  95. data/lib/aia/input_collector.rb +3 -3
  96. data/lib/aia/layered_orchestrator.rb +471 -0
  97. data/lib/aia/logger.rb +45 -25
  98. data/lib/aia/mcp_config_normalizer.rb +35 -0
  99. data/lib/aia/mcp_connection_manager.rb +315 -0
  100. data/lib/aia/mcp_discovery.rb +44 -0
  101. data/lib/aia/mcp_grouper.rb +33 -0
  102. data/lib/aia/mcp_server_config.rb +30 -0
  103. data/lib/aia/mcp_utility.rb +60 -0
  104. data/lib/aia/mention_router.rb +217 -0
  105. data/lib/aia/model_alias_registry.rb +97 -0
  106. data/lib/aia/model_switch_handler.rb +100 -0
  107. data/lib/aia/network_builder.rb +160 -0
  108. data/lib/aia/network_memory_manager.rb +55 -0
  109. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  110. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  111. data/lib/aia/pipeline_orchestrator.rb +272 -0
  112. data/lib/aia/plugin_loader.rb +170 -0
  113. data/lib/aia/plugin_monitor.rb +211 -0
  114. data/lib/aia/prompt_decomposer.rb +159 -0
  115. data/lib/aia/prompt_handler.rb +56 -82
  116. data/lib/aia/robot_builder.rb +51 -0
  117. data/lib/aia/robot_factory.rb +338 -0
  118. data/lib/aia/robot_namer.rb +110 -0
  119. data/lib/aia/session.rb +87 -17
  120. data/lib/aia/session_tracker.rb +207 -0
  121. data/lib/aia/similarity_scorer.rb +41 -0
  122. data/lib/aia/skill_utils.rb +105 -1
  123. data/lib/aia/spawn_handler.rb +129 -0
  124. data/lib/aia/spawn_spec_parser.rb +65 -0
  125. data/lib/aia/special_mode_handler.rb +322 -0
  126. data/lib/aia/speech.rb +67 -0
  127. data/lib/aia/startup_coordinator.rb +151 -0
  128. data/lib/aia/streaming_runner.rb +172 -0
  129. data/lib/aia/system_prompt_assembler.rb +92 -0
  130. data/lib/aia/task_coordinator.rb +207 -0
  131. data/lib/aia/task_decomposer.rb +57 -0
  132. data/lib/aia/task_executor.rb +51 -0
  133. data/lib/aia/tfidf_math.rb +27 -0
  134. data/lib/aia/timing.rb +15 -0
  135. data/lib/aia/tool_filter/tfidf.rb +116 -0
  136. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  137. data/lib/aia/tool_filter.rb +83 -0
  138. data/lib/aia/tool_filter_registry.rb +30 -0
  139. data/lib/aia/tool_filter_strategy.rb +146 -0
  140. data/lib/aia/tool_introspection.rb +17 -0
  141. data/lib/aia/tool_loader.rb +216 -0
  142. data/lib/aia/tool_utility.rb +30 -0
  143. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  144. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  145. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  146. data/lib/aia/tools/task_board_tool.rb +115 -0
  147. data/lib/aia/trakflow_bridge.rb +175 -0
  148. data/lib/aia/turn_state.rb +95 -0
  149. data/lib/aia/ui_presenter.rb +182 -206
  150. data/lib/aia/utility.rb +136 -87
  151. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +9 -9
  152. data/lib/aia/verification_network.rb +57 -0
  153. data/lib/aia.rb +124 -63
  154. data/mkdocs.yml +1 -0
  155. metadata +187 -58
  156. data/justfile +0 -215
  157. data/lib/aia/adapter/chat_execution.rb +0 -242
  158. data/lib/aia/adapter/error_handler.rb +0 -68
  159. data/lib/aia/adapter/gem_activator.rb +0 -57
  160. data/lib/aia/adapter/mcp_connector.rb +0 -274
  161. data/lib/aia/adapter/modality_handlers.rb +0 -167
  162. data/lib/aia/adapter/model_registry.rb +0 -81
  163. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  164. data/lib/aia/adapter/provider_configurator.rb +0 -59
  165. data/lib/aia/adapter/tool_filter.rb +0 -85
  166. data/lib/aia/adapter/tool_loader.rb +0 -90
  167. data/lib/aia/chat_processor_service.rb +0 -178
  168. data/lib/aia/prompt_pipeline.rb +0 -183
  169. data/lib/aia/ruby_llm_adapter.rb +0 -95
  170. data/lib/extensions/openstruct_merge.rb +0 -48
  171. data/lib/extensions/ruby_llm/.irbrc +0 -56
  172. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  173. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  174. data/lib/refinements/string.rb +0 -16
  175. data/main.just +0 -76
@@ -11,6 +11,7 @@ require_relative 'model_spec'
11
11
  require_relative '../skill_utils'
12
12
 
13
13
  module AIA
14
+ # rubocop:disable Metrics/ModuleLength
14
15
  module CLIParser
15
16
  class << self
16
17
  # Parse CLI arguments and return a hash of overrides
@@ -23,8 +24,8 @@ module AIA
23
24
  parser = create_option_parser(options)
24
25
  parser.parse!
25
26
  rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
26
- warn "ERROR: #{e.message}"
27
- warn " use --help for usage report"
27
+ $stderr.puts "ERROR: #{e.message}"
28
+ $stderr.puts " use --help for usage report"
28
29
  exit 1
29
30
  end
30
31
 
@@ -51,10 +52,11 @@ module AIA
51
52
  end
52
53
 
53
54
  def setup_banner(opts)
54
- opts.banner = "Usage: aia [options] [PROMPT_ID] [CONTEXT_FILE]*\n" +
55
- " aia [options] --chat [PROMPT_ID] [CONTEXT_FILE]*"
55
+ opts.banner = "Usage: aia [options] [PROMPT_ID] [CONTEXT_FILE]*\n " \
56
+ "aia [options] --chat [PROMPT_ID] [CONTEXT_FILE]*"
56
57
  end
57
58
 
59
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
58
60
  def setup_mode_options(opts, options)
59
61
  opts.separator "\nMode Options:"
60
62
 
@@ -64,17 +66,38 @@ module AIA
64
66
 
65
67
  opts.on("-f", "--fuzzy", "Use fuzzy matching for prompt search") do
66
68
  unless system("which fzf > /dev/null 2>&1")
67
- warn "Error: 'fzf' is not installed. Please install 'fzf' to use the --fuzzy option."
69
+ $stderr.puts "Error: 'fzf' is not installed. Please install 'fzf' to use the --fuzzy option."
68
70
  exit 1
69
71
  end
70
72
  options[:fuzzy] = true
71
73
  end
72
74
 
73
- opts.on("--terse", "[DEPRECATED] No longer supported") do
74
- warn "Warning: --terse is deprecated and has no effect."
75
+ opts.on("--expert-routing", "Route each prompt to a specialist robot based on domain classification") do
76
+ options[:expert_routing] = true
77
+ end
78
+
79
+ opts.on("--track-pipeline", "Track pipeline execution in TrakFlow") do
80
+ options[:track_pipeline] = true
81
+ end
82
+
83
+ opts.on("--concurrent-auto", "Automatically parallelize independent MCP servers") do
84
+ options[:concurrent_auto] = true
85
+ end
86
+
87
+ opts.on("-A", "--[no-]auto-tool-filter", "Use TF-IDF similarity-based tool filtering (default: on)") do |v|
88
+ options[:auto_tool_filter] = v
89
+ end
90
+
91
+ opts.on("--load", "Load persisted tool filter databases from ~/.config/aia/") do
92
+ options[:tool_filter_load] = true
93
+ end
94
+
95
+ opts.on("--save", "Save tool filter databases to ~/.config/aia/") do
96
+ options[:tool_filter_save] = true
75
97
  end
76
98
  end
77
99
 
100
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
78
101
  def setup_model_options(opts, options)
79
102
  opts.separator "\nModel Options:"
80
103
 
@@ -82,8 +105,10 @@ module AIA
82
105
  list_available_models(query)
83
106
  end
84
107
 
85
- opts.on("-m MODEL", "--model MODEL", "Set LLM model(s) to use. Format: MODEL[=ROLE][,MODEL[=ROLE]]...") do |model_string|
86
- options[:models] = parse_models_with_roles(model_string)
108
+ opts.on("-m MODEL", "--model MODEL",
109
+ "Set LLM model(s). Format: MODEL[=ROLE][,MODEL[=ROLE]]... " \
110
+ "Multiple models run in parallel; use --consensus to synthesize results") do |model_string|
111
+ options[:models] = (options[:models] || []) + parse_models_with_roles(model_string)
87
112
  end
88
113
 
89
114
  opts.on("--[no-]consensus", "Enable/disable consensus mode for multi-model responses") do |consensus|
@@ -104,6 +129,7 @@ module AIA
104
129
  end
105
130
  end
106
131
 
132
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
107
133
  def setup_file_options(opts, options)
108
134
  opts.separator "\nFile & Output Options:"
109
135
 
@@ -112,13 +138,13 @@ module AIA
112
138
  end
113
139
 
114
140
  opts.on("-o", "--[no-]output [FILE]", "Write response to FILE (default: temp.md; --no-output to disable)") do |file|
115
- if file == false
116
- options[:output] = nil
117
- elsif file.nil?
118
- options[:output] = 'temp.md'
119
- else
120
- options[:output] = File.expand_path(file, Dir.pwd)
121
- end
141
+ options[:output] = if file == false
142
+ nil
143
+ elsif file.nil?
144
+ 'temp.md'
145
+ else
146
+ File.expand_path(file, Dir.pwd)
147
+ end
122
148
  end
123
149
 
124
150
  opts.on("-a", "--[no-]append", "Append to output file instead of overwriting") do |append|
@@ -134,6 +160,8 @@ module AIA
134
160
  end
135
161
  end
136
162
 
163
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
164
+ # :reek:DuplicateMethodCall -- options[:pipeline] is appended inside two separate OptionParser closures
137
165
  def setup_prompt_options(opts, options)
138
166
  opts.separator "\nPrompt Options:"
139
167
 
@@ -141,6 +169,10 @@ module AIA
141
169
  options[:prompts_dir] = dir
142
170
  end
143
171
 
172
+ opts.on("--plugins-dir DIR", "Set directory containing AIA plugin files") do |dir|
173
+ options[:plugins_dir] = dir
174
+ end
175
+
144
176
  opts.on("--roles-prefix PREFIX", "Set subdirectory name for role files (default: roles)") do |prefix|
145
177
  options[:roles_prefix] = prefix
146
178
  end
@@ -149,6 +181,19 @@ module AIA
149
181
  options[:role] = role
150
182
  end
151
183
 
184
+ opts.on("--skills-dir DIR", "Set directory containing skill subdirectories") do |dir|
185
+ options[:skills_dir] = dir
186
+ end
187
+
188
+ opts.on("-s", "--skill SKILL_IDS", "Append skill(s) to prompt; comma-separated IDs or paths") do |skill_ids|
189
+ options[:skills] ||= []
190
+ options[:skills] += skill_ids.split(',').map(&:strip).reject(&:empty?)
191
+ end
192
+
193
+ opts.on("--list-skills", "List available skills and exit") do
194
+ options[:list_skills] = true
195
+ end
196
+
152
197
  opts.on("-n", "--next PROMPT_ID", "Set next prompt to process") do |next_prompt|
153
198
  options[:pipeline] ||= []
154
199
  options[:pipeline] << next_prompt
@@ -159,32 +204,9 @@ module AIA
159
204
  options[:pipeline] += pipeline.split(',').map(&:strip)
160
205
  end
161
206
 
162
-
163
207
  opts.on("--system-prompt PROMPT_ID", "Set system prompt for chat sessions") do |prompt_id|
164
208
  options[:system_prompt] = prompt_id
165
209
  end
166
-
167
- opts.on('--regex PATTERN', '[DEPRECATED] Parameter regex (PM v1.0.0 uses ERB parameters)') do |pattern|
168
- warn "Warning: --regex is deprecated. PM v1.0.0 uses ERB parameters (<%= param %>)."
169
- options[:parameter_regex] = pattern
170
- end
171
-
172
- opts.on("--skills-dir DIR", "Set directory containing skill subdirectories") do |dir|
173
- options[:skills_dir] = dir
174
- end
175
-
176
- opts.on("--skills-prefix PREFIX", "Set subdirectory name for skill files (default: skills)") do |prefix|
177
- options[:skills_prefix] = prefix
178
- end
179
-
180
- opts.on("-s", "--skill SKILL_IDS", "Prepend skill(s) to prompt (comma-separated IDs or paths)") do |ids|
181
- options[:skills] ||= []
182
- options[:skills] += ids.split(',').map(&:strip)
183
- end
184
-
185
- opts.on("--list-skills", "List available skills and exit") do
186
- options[:list_skills] = true
187
- end
188
210
  end
189
211
 
190
212
  def setup_ai_parameters(opts, options)
@@ -194,7 +216,7 @@ module AIA
194
216
  options[:temperature] = temp
195
217
  end
196
218
 
197
- opts.on("--max-tokens TOKENS", Integer, "Set maximum tokens for text generation (default: 2048)") do |tokens|
219
+ opts.on("--max-tokens TOKENS", Integer, "Set maximum tokens for text generation (default: 32767)") do |tokens|
198
220
  options[:max_tokens] = tokens
199
221
  end
200
222
 
@@ -211,6 +233,7 @@ module AIA
211
233
  end
212
234
  end
213
235
 
236
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
214
237
  def setup_audio_image_options(opts, options)
215
238
  opts.separator "\nAudio & Image Options:"
216
239
 
@@ -218,10 +241,22 @@ module AIA
218
241
  options[:speak] = true
219
242
  end
220
243
 
221
- opts.on("--voice VOICE", "Set voice for speech output (default: alloy)") do |voice|
244
+ opts.on("--voice VOICE", "Set voice for speech output") do |voice|
222
245
  options[:voice] = voice
223
246
  end
224
247
 
248
+ opts.on("--speak-command CMD", "Command used for TTS (default: say)") do |cmd|
249
+ options[:speak_command] = cmd
250
+ end
251
+
252
+ opts.on("--paint", "Generate an image from the prompt instead of a text response") do
253
+ options[:paint] = true
254
+ end
255
+
256
+ opts.on("--im", "--image-model MODEL", "Set image model for generation (default: dall-e-3)") do |model|
257
+ options[:image_model] = model
258
+ end
259
+
225
260
  opts.on("--is", "--image-size SIZE", "Set image size for generation (default: 1024x1024)") do |size|
226
261
  options[:image_size] = size
227
262
  end
@@ -235,9 +270,14 @@ module AIA
235
270
  end
236
271
  end
237
272
 
273
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
238
274
  def setup_tool_options(opts, options)
239
275
  opts.separator "\nTool & Extension Options:"
240
276
 
277
+ opts.on('--allow-ruby-eval', 'Enable /ruby directive (executes arbitrary Ruby code with full process privileges)') do
278
+ options[:allow_ruby_eval] = true
279
+ end
280
+
241
281
  opts.on("--rq LIBS", "--require LIBS", "Require Ruby libraries for Ruby directive") do |libs|
242
282
  options[:require_libs] ||= []
243
283
  options[:require_libs] += libs.split(',')
@@ -264,11 +304,18 @@ module AIA
264
304
 
265
305
  def setup_utility_options(opts, options)
266
306
  opts.separator "\nUtility Options:"
307
+ setup_logging_options(opts, options)
308
+ setup_output_options(opts, options)
309
+ setup_mcp_options(opts, options)
310
+ setup_meta_options(opts, options)
311
+ end
267
312
 
313
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
314
+ def setup_logging_options(opts, options)
268
315
  opts.on("--log-level LEVEL", "Set log level (debug|info|warn|error|fatal)") do |level|
269
316
  level = level.downcase
270
317
  unless %w[debug info warn error fatal].include?(level)
271
- warn "ERROR: Invalid log level '#{level}'. Must be one of: debug, info, warn, error, fatal"
318
+ $stderr.puts "ERROR: Invalid log level '#{level}'. Must be one of: debug, info, warn, error, fatal"
272
319
  exit 1
273
320
  end
274
321
  options[:log_level_override] = level
@@ -296,6 +343,22 @@ module AIA
296
343
  opts.on("-v", "--[no-]verbose", "Enable verbose output") do |value|
297
344
  options[:verbose] = value
298
345
  end
346
+ end
347
+
348
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
349
+ def setup_output_options(opts, options)
350
+ opts.on("--[no-]thinking", "Show raw thinking/reasoning blocks in output (default: off)") do |v|
351
+ options[:thinking] = v
352
+ end
353
+
354
+ opts.on("--tokens", "Display token usage and elapsed response time after each turn") do
355
+ options[:tokens] = true
356
+ end
357
+
358
+ opts.on("--cost", "Display cost calculations and elapsed time (implies --tokens)") do
359
+ options[:cost] = true
360
+ options[:tokens] = true
361
+ end
299
362
 
300
363
  opts.on("--refresh DAYS", Integer, "Set refresh interval (days) for cached models list (default: 7)") do |days|
301
364
  options[:refresh] = days || 0
@@ -308,16 +371,10 @@ module AIA
308
371
  opts.on("--completion SHELL", "Generate shell completion script (bash|zsh|fish) and exit") do |shell|
309
372
  options[:completion] = shell
310
373
  end
374
+ end
311
375
 
312
- opts.on("--tokens", "Display token usage") do
313
- options[:tokens] = true
314
- end
315
-
316
- opts.on("--cost", "Display cost calculations (implies --tokens)") do
317
- options[:cost] = true
318
- options[:tokens] = true # --cost implies --tokens
319
- end
320
-
376
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
377
+ def setup_mcp_options(opts, options)
321
378
  opts.on("--mcp FILE", "Load MCP server(s) from JSON file (repeatable)") do |file|
322
379
  options[:mcp_files] ||= []
323
380
  options[:mcp_files] << file
@@ -340,55 +397,65 @@ module AIA
340
397
  options[:mcp_skip] ||= []
341
398
  options[:mcp_skip] += names.split(',').map(&:strip)
342
399
  end
400
+ end
343
401
 
402
+ def setup_meta_options(opts, _options)
344
403
  opts.on("--version", "Show version and exit") do
345
404
  puts AIA::VERSION
346
405
  exit
347
406
  end
348
407
 
349
408
  opts.on("-h", "--help", "Show this help and exit") do
350
- puts <<~HELP
351
-
352
- AIA - Your AI Assistant (v#{AIA::VERSION})
353
- - Manage AI prompts with embedded directives
354
- - Integrate with shell and Ruby (ERB) processing
355
- - Run batch processes and prompt pipelines
356
- - Engage in interactive chat sessions
357
- - Use custom tools and MCP servers
358
-
359
- HELP
360
-
361
- puts opts
362
-
363
- puts <<~EXTRA
364
-
365
- Explore Further:
366
- - AIA GitHub Repository: https://github.com/MadBomber/aia
367
- - AIA Documentation: https://madbomber.github.io/aia
368
- - AIA Changelog: https://github.com/MadBomber/aia/blob/main/CHANGELOG.md
369
- - AIA Examples: https://github.com/MadBomber/aia/tree/main/examples
370
- - Report an Issue: https://github.com/MadBomber/aia/issues
371
- - RubyLLM Documentation: https://rubyllm.com
372
- - RubyLLM Tool Docs: https://rubyllm.com/guides/tools
373
- - PromptManager Docs: https://madbomber.github.io/prompt_manager
374
- - ERB Documentation: https://docs.ruby-lang.org/en/master/ERB.html
375
- - MCP Specification: https://modelcontextprotocol.io
376
- - MCP Client Docs: https://github.com/patvice/ruby_llm-mcp/blob/main/README.md
377
-
378
- EXTRA
379
-
409
+ print_help(opts)
380
410
  exit
381
411
  end
382
412
  end
383
413
 
414
+ def print_help(opts)
415
+ puts <<~HELP
416
+
417
+ AIA - Your AI Assistant (v#{AIA::VERSION})
418
+ - Manage AI prompts with embedded directives
419
+ - Integrate with shell and Ruby (ERB) processing
420
+ - Run batch processes and prompt pipelines
421
+ - Engage in interactive chat sessions
422
+ - Use custom tools and MCP servers
423
+ - Compare the same prompt across multiple models (--model m1,m2,m3 [--consensus])
424
+ - Measure response time and token usage with --tokens; add cost with --cost
425
+ - Benchmark concurrent vs. serial performance (/decompose reports speedup metrics)
426
+
427
+ HELP
428
+
429
+ puts opts
430
+
431
+ puts <<~EXTRA
432
+
433
+ Explore Further:
434
+ - AIA GitHub Repository: https://github.com/MadBomber/aia
435
+ - AIA Documentation: https://madbomber.github.io/aia
436
+ - AIA Changelog: https://github.com/MadBomber/aia/blob/main/CHANGELOG.md
437
+ - AIA Examples: https://github.com/MadBomber/aia/tree/main/examples
438
+ - Report an Issue: https://github.com/MadBomber/aia/issues
439
+ - RubyLLM Documentation: https://rubyllm.com
440
+ - RubyLLM Tool Docs: https://rubyllm.com/guides/tools
441
+ - PromptManager Docs: https://madbomber.github.io/prompt_manager
442
+ - ERB Documentation: https://docs.ruby-lang.org/en/master/ERB.html
443
+ - MCP Specification: https://modelcontextprotocol.io
444
+ - MCP Client Docs: https://github.com/patvice/ruby_llm-mcp/blob/main/README.md
445
+
446
+ EXTRA
447
+ end
448
+
384
449
  # Parse model string into array of ModelSpec-compatible hashes
385
450
  #
386
451
  # @param model_string [String] comma-separated models with optional roles
387
452
  # @return [Array<Hash>] array of model specs
453
+ # :reek:TooManyStatements -- one-pass parser over MODEL[=ROLE] specs sharing the instance counter
388
454
  def parse_models_with_roles(model_string)
389
455
  models = []
390
456
  model_counts = Hash.new(0)
391
457
 
458
+ # rubocop:disable-next Metrics/BlockLength
392
459
  model_string.split(',').each do |spec|
393
460
  spec.strip!
394
461
 
@@ -429,42 +496,42 @@ module AIA
429
496
  end
430
497
 
431
498
  def validate_role_exists(role_id)
432
- if AIA::SkillUtils.path_based_id?(role_id)
433
- expanded = File.expand_path(role_id)
434
- expanded += '.md' if File.extname(expanded).empty?
435
- raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
436
- return
437
- end
499
+ return validate_role_path!(role_id) if AIA::SkillUtils.path_based_id?(role_id)
438
500
 
439
- prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(ENV['HOME'], '.prompts'))
501
+ prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
440
502
  roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
441
-
442
- unless role_id.start_with?(roles_prefix)
443
- role_id = "#{roles_prefix}/#{role_id}"
444
- end
503
+ role_id = "#{roles_prefix}/#{role_id}" unless role_id.start_with?(roles_prefix)
445
504
 
446
505
  role_file_path = File.join(prompts_dir, "#{role_id}.md")
506
+ return if File.exist?(role_file_path)
447
507
 
448
- unless File.exist?(role_file_path)
449
- available_roles = list_available_role_names(prompts_dir, roles_prefix)
508
+ raise ArgumentError, role_not_found_message(role_file_path, prompts_dir, roles_prefix)
509
+ end
450
510
 
451
- error_msg = "Role file not found: #{role_file_path}\n\n"
511
+ # A path-based role id must resolve to an existing file.
512
+ def validate_role_path!(role_id)
513
+ expanded = File.expand_path(role_id)
514
+ expanded += '.md' if File.extname(expanded).empty?
515
+ raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
516
+ end
452
517
 
453
- if available_roles.empty?
454
- error_msg += "No roles directory found at #{File.join(prompts_dir, roles_prefix)}\n"
455
- error_msg += "Create the directory and add role files to use this feature."
456
- else
457
- error_msg += "Available roles:\n"
458
- error_msg += available_roles.map { |r| " - #{r}" }.join("\n")
459
- error_msg += "\n\nCreate the role file or use an existing role."
460
- end
518
+ def role_not_found_message(role_file_path, prompts_dir, roles_prefix)
519
+ available_roles = list_available_role_names(prompts_dir, roles_prefix)
520
+ message = "Role file not found: #{role_file_path}\n\n"
461
521
 
462
- raise ArgumentError, error_msg
522
+ if available_roles.empty?
523
+ message + "No roles directory found at #{File.join(prompts_dir, roles_prefix)}\n" \
524
+ "Create the directory and add role files to use this feature."
525
+ else
526
+ message + "Available roles:\n" +
527
+ available_roles.map { |r| " - #{r}" }.join("\n") +
528
+ "\n\nCreate the role file or use an existing role."
463
529
  end
464
530
  end
465
531
 
532
+ # :reek:TooManyStatements -- sequential terminal report: guards, then a markdown table per role; exits after printing
466
533
  def list_available_roles
467
- prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(ENV['HOME'], '.prompts'))
534
+ prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
468
535
  roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
469
536
  roles_dir = File.join(prompts_dir, roles_prefix)
470
537
 
@@ -484,13 +551,16 @@ module AIA
484
551
 
485
552
  roles.each do |role_id|
486
553
  role_file = File.join(roles_dir, "#{role_id}.md")
487
- fm = AIA::SkillUtils.parse_front_matter(role_file)
554
+ front_matter = AIA::SkillUtils.parse_front_matter(role_file)
488
555
 
489
556
  puts "## #{role_id}"
490
557
  puts
491
558
  puts "| Key | Value |"
492
559
  puts "|-----|-------|"
493
- fm.each { |key, value| puts "| #{key} | #{value} |" }
560
+ front_matter.each do |key, value|
561
+ safe_value = value.to_s.gsub('|', '\\|')
562
+ puts "| #{key} | #{safe_value} |"
563
+ end
494
564
  puts
495
565
  end
496
566
  end
@@ -500,74 +570,76 @@ module AIA
500
570
  return [] unless Dir.exist?(roles_dir)
501
571
 
502
572
  Dir.glob("**/*.md", base: roles_dir)
503
- .map { |f| f.chomp('.md') }
504
- .reject { |f| f.split('/').any? { |part| part.start_with?('_') } }
505
- .sort
573
+ .map { |f| f.chomp('.md') }
574
+ .reject { |f| f.split('/').any? { |part| part.start_with?('_') } }
575
+ .sort
506
576
  end
507
577
 
578
+ # :reek:TooManyStatements -- sequential --available-models report: parse query, print matches, summary, exit
508
579
  def list_available_models(query)
509
580
  require 'ruby_llm'
510
581
 
511
- if query.nil?
512
- query = []
513
- else
514
- query = query.split(',')
515
- end
582
+ query = query.nil? ? [] : query.split(',')
583
+ modality_terms, substring_terms = parse_model_query(query)
516
584
 
517
585
  header = "\nAvailable LLMs"
518
586
  header += " for #{query.join(' and ')}" if query.any?
519
-
520
587
  puts header + ':'
521
588
  puts
522
589
 
523
- q1 = query.select { |q| q.include?('_to_') }.map { |q| q[0] == ':' ? q[1..] : q }
524
- q2 = query.reject { |q| q.include?('_to_') }
525
-
526
- counter = 0
527
-
528
- RubyLLM.models.all.each do |llm|
529
- inputs = llm.modalities.input.join(',')
530
- outputs = llm.modalities.output.join(',')
531
- entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}"
590
+ counter = print_matching_models(modality_terms, substring_terms)
532
591
 
533
- if query.nil? || query.empty?
534
- counter += 1
535
- puts entry
536
- next
537
- end
592
+ puts if counter.positive?
593
+ puts "#{counter} LLMs matching your query"
594
+ puts
538
595
 
539
- show_it = true
540
- q1.each { |q| show_it &&= llm.modalities.send("#{q}?") }
541
- q2.each { |q| show_it &&= entry.include?(q) }
596
+ exit
597
+ end
542
598
 
543
- if show_it
544
- counter += 1
545
- puts entry
546
- end
599
+ # Print each matching model entry; returns the number printed.
600
+ def print_matching_models(modality_terms, substring_terms)
601
+ RubyLLM.models.all.count do |llm|
602
+ entry = format_model_entry(llm)
603
+ visible = model_entry_visible?(llm, entry, modality_terms, substring_terms)
604
+ puts entry if visible
605
+ visible
547
606
  end
607
+ end
548
608
 
549
- puts if counter > 0
550
- puts "#{counter} LLMs matching your query"
551
- puts
609
+ # Split a query into modality terms (e.g. "text_to_text", leading ':'
610
+ # stripped) and plain substring terms.
611
+ def parse_model_query(query)
612
+ modality, substrings = query.partition { |q| q.include?('_to_') }
613
+ [modality.map { |q| q.delete_prefix(':') }, substrings]
614
+ end
552
615
 
553
- exit
616
+ def format_model_entry(llm)
617
+ modalities = llm.modalities
618
+ "- #{llm.id} (#{llm.provider}) #{modalities.input.join(',')} to #{modalities.output.join(',')}"
619
+ end
620
+
621
+ def model_entry_visible?(llm, entry, modality_terms, substring_terms)
622
+ modality_terms.all? { |q| llm.modalities.send("#{q}?") } &&
623
+ substring_terms.all? { |q| entry.include?(q) }
554
624
  end
625
+ # rubocop:enable Metrics/ModuleLength
555
626
 
627
+ # :reek:DuplicateMethodCall -- each `exit 1` terminates a distinct validation failure; there is no value to hoist
556
628
  def process_tools_paths(path_list)
557
629
  paths = []
558
630
 
559
631
  if path_list.empty?
560
- warn "No list of paths for --tools option"
632
+ $stderr.puts "No list of paths for --tools option"
561
633
  exit 1
562
634
  end
563
635
 
564
636
  path_list.split(',').map(&:strip).uniq.each do |a_path|
565
637
  if File.exist?(a_path)
566
638
  if File.file?(a_path)
567
- if '.rb' == File.extname(a_path)
639
+ if File.extname(a_path) == '.rb'
568
640
  paths << a_path
569
641
  else
570
- warn "file should have *.rb extension: #{a_path}"
642
+ $stderr.puts "file should have *.rb extension: #{a_path}"
571
643
  exit 1
572
644
  end
573
645
  elsif File.directory?(a_path)
@@ -575,7 +647,7 @@ module AIA
575
647
  paths += rb_files
576
648
  end
577
649
  else
578
- warn "file/dir path is not valid: #{a_path}"
650
+ $stderr.puts "file/dir path is not valid: #{a_path}"
579
651
  exit 1
580
652
  end
581
653
  end