aia 2.0.0.0.pre.alpha → 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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +5 -1
  3. data/.loki +3 -223
  4. data/.reek.yml +160 -0
  5. data/.rubocop_strict.yml +15 -0
  6. data/.version +1 -1
  7. data/CHANGELOG.md +10 -5
  8. data/README.md +1 -1
  9. data/Rakefile +0 -113
  10. data/_typos.toml +10 -0
  11. data/aia.gemspec +92 -0
  12. data/config/aia.yml +13 -0
  13. data/lib/aia/chat_loop.rb +8 -52
  14. data/lib/aia/config/cli_parser.rb +66 -57
  15. data/lib/aia/config/mcp_parser.rb +27 -19
  16. data/lib/aia/config/validator.rb +83 -94
  17. data/lib/aia/config.rb +3 -2
  18. data/lib/aia/content_extractor.rb +2 -0
  19. data/lib/aia/cost_calculator.rb +1 -0
  20. data/lib/aia/debate_handler.rb +28 -20
  21. data/lib/aia/delegate_handler.rb +7 -3
  22. data/lib/aia/directive.rb +10 -8
  23. data/lib/aia/directives/configuration_directives.rb +115 -101
  24. data/lib/aia/directives/context_directives.rb +31 -28
  25. data/lib/aia/directives/execution_directives.rb +18 -13
  26. data/lib/aia/directives/model_directives.rb +134 -154
  27. data/lib/aia/directives/trakflow_directives.rb +27 -9
  28. data/lib/aia/directives/utility_directives.rb +71 -71
  29. data/lib/aia/directives/web_and_file_directives.rb +63 -50
  30. data/lib/aia/layered_orchestrator.rb +66 -43
  31. data/lib/aia/logger.rb +21 -21
  32. data/lib/aia/mcp_connection_manager.rb +50 -40
  33. data/lib/aia/mcp_server_config.rb +30 -0
  34. data/lib/aia/mcp_utility.rb +7 -4
  35. data/lib/aia/mention_router.rb +7 -50
  36. data/lib/aia/network_builder.rb +10 -5
  37. data/lib/aia/network_memory_manager.rb +6 -6
  38. data/lib/aia/pipeline_orchestrator.rb +20 -10
  39. data/lib/aia/plugin_monitor.rb +9 -6
  40. data/lib/aia/prompt_decomposer.rb +5 -3
  41. data/lib/aia/prompt_handler.rb +43 -49
  42. data/lib/aia/robot_factory.rb +12 -8
  43. data/lib/aia/robot_namer.rb +2 -8
  44. data/lib/aia/session.rb +7 -3
  45. data/lib/aia/session_tracker.rb +39 -41
  46. data/lib/aia/similarity_scorer.rb +5 -3
  47. data/lib/aia/special_mode_handler.rb +52 -32
  48. data/lib/aia/speech.rb +67 -0
  49. data/lib/aia/startup_coordinator.rb +1 -0
  50. data/lib/aia/streaming_runner.rb +6 -3
  51. data/lib/aia/system_prompt_assembler.rb +11 -7
  52. data/lib/aia/task_coordinator.rb +10 -5
  53. data/lib/aia/timing.rb +15 -0
  54. data/lib/aia/tool_filter.rb +1 -0
  55. data/lib/aia/tool_filter_strategy.rb +28 -25
  56. data/lib/aia/tool_introspection.rb +17 -0
  57. data/lib/aia/tool_loader.rb +12 -6
  58. data/lib/aia/tools/task_board_tool.rb +1 -0
  59. data/lib/aia/trakflow_bridge.rb +5 -3
  60. data/lib/aia/turn_state.rb +1 -0
  61. data/lib/aia/ui_presenter.rb +67 -59
  62. data/lib/aia/utility.rb +5 -3
  63. data/lib/aia/variable_input_collector.rb +1 -0
  64. data/lib/aia/verification_network.rb +1 -2
  65. data/lib/aia.rb +16 -0
  66. metadata +15 -9
  67. data/.quality/flay_baseline.txt +0 -1
  68. data/.quality/flog_baseline.txt +0 -29
  69. data/.quality/reek_baseline.txt +0 -80
@@ -29,10 +29,29 @@ module AIA
29
29
  alias llms available_models
30
30
 
31
31
  desc "Compare responses from multiple models"
32
- # rubocop:disable Metrics/MethodLength
33
32
  def compare(args, context_manager = nil)
34
- return 'Error: No prompt provided for comparison' if args.empty?
33
+ prompt, models = parse_compare_args(args)
35
34
 
35
+ return report_error('Error: No prompt provided for comparison') unless prompt
36
+ return report_error('Error: No models specified. Use --models model1,model2,model3') if models.empty?
37
+
38
+ puts "\nComparing responses for: #{prompt}\n"
39
+ puts '=' * 80
40
+
41
+ models.each { |model_name| compare_with_model(model_name.strip, prompt) }
42
+
43
+ puts "\n" + ('=' * 80)
44
+ puts "\nComparison complete!"
45
+
46
+ ''
47
+ end
48
+ alias cmp compare
49
+
50
+ # --- helpers (no desc → not registered) ---
51
+
52
+ # Split a //compare arg list into [prompt, models]; the first non-flag
53
+ # token is the prompt, `--models a,b,c` supplies the model list.
54
+ def parse_compare_args(args)
36
55
  prompt = nil
37
56
  models = []
38
57
 
@@ -47,42 +66,26 @@ module AIA
47
66
  end
48
67
  end
49
68
 
50
- return 'Error: No prompt provided for comparison' unless prompt
51
- return 'Error: No models specified. Use --models model1,model2,model3' if models.empty?
52
-
53
- puts "\nComparing responses for: #{prompt}\n"
54
- puts '=' * 80
55
-
56
- results = {}
57
-
58
- models.each do |model_name|
59
- model_name.strip!
60
- puts "\n🤖 **#{model_name}:**"
61
- puts '-' * 40
69
+ [prompt, models]
70
+ end
62
71
 
63
- begin
64
- chat = RubyLLM.chat(model: model_name)
65
- response = chat.ask(prompt)
66
- content = response.content
72
+ def compare_with_model(model_name, prompt)
73
+ puts "\n🤖 **#{model_name}:**"
74
+ puts '-' * 40
67
75
 
68
- puts content
69
- results[model_name] = content
70
- rescue StandardError => e
71
- error_msg = "Error with #{model_name}: #{e.message}"
72
- puts error_msg
73
- results[model_name] = error_msg
74
- end
75
- end
76
-
77
- puts "\n" + ('=' * 80)
78
- puts "\nComparison complete!"
79
-
80
- ''
76
+ response = RubyLLM.chat(model: model_name).ask(prompt)
77
+ puts response.content
78
+ rescue StandardError => e
79
+ puts "Error with #{model_name}: #{e.message}"
81
80
  end
82
- # rubocop:enable Metrics/MethodLength
83
- alias cmp compare
84
81
 
85
- # --- helpers (no desc → not registered) ---
82
+ # Log and print a directive error, then return nil so the chat loop
83
+ # skips forwarding it to the robot (an error is not conversational output).
84
+ def report_error(msg)
85
+ AIA::LoggerManager.aia_logger.error(msg)
86
+ puts msg
87
+ nil
88
+ end
86
89
 
87
90
  def show_local_models(current_models, positive_terms = nil, negative_terms = nil)
88
91
  require 'net/http'
@@ -105,110 +108,93 @@ module AIA
105
108
  end
106
109
  end
107
110
 
108
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
111
+ # :reek:TooManyStatements -- sequential terminal report: fetch, connection guards, filtered listing
109
112
  def show_ollama_models(api_base, positive_terms = nil, negative_terms = nil)
110
113
  positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
111
114
 
112
- begin
113
- uri = URI("#{api_base}/api/tags")
114
- http = Net::HTTP.new(uri.host, uri.port)
115
- http.open_timeout = 5
116
- http.read_timeout = 5
117
- response = http.request(Net::HTTP::Get.new(uri))
115
+ data = fetch_json("#{api_base}/api/tags")
116
+ if data.nil?
117
+ puts "❌ Cannot connect to Ollama at #{api_base}"
118
+ return
119
+ end
118
120
 
119
- unless response.is_a?(Net::HTTPSuccess)
120
- puts "❌ Cannot connect to Ollama at #{api_base}"
121
- return
122
- end
121
+ models = data['models'] || []
122
+ if models.empty?
123
+ puts "No Ollama models found"
124
+ return
125
+ end
123
126
 
124
- data = JSON.parse(response.body)
125
- models = data['models'] || []
127
+ puts "Ollama Models (#{api_base}):"
128
+ puts "-" * 60
126
129
 
127
- if models.empty?
128
- puts "No Ollama models found"
129
- return
130
- end
130
+ entries = models.map { |model| format_ollama_entry(model) }
131
+ print_filtered_entries(entries, positive_terms, negative_terms, "Ollama model(s) available")
132
+ rescue StandardError => e
133
+ puts "❌ Error fetching Ollama models: #{e.message}"
134
+ end
131
135
 
132
- puts "Ollama Models (#{api_base}):"
133
- puts "-" * 60
134
-
135
- counter = 0
136
- models.each do |model|
137
- name = model['name']
138
- size = model['size'] ? format_bytes(model['size']) : 'unknown'
139
- modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
140
-
141
- entry = "- ollama/#{name} (size: #{size}, modified: #{modified})"
142
- entry_lc = entry.downcase
143
-
144
- # entry_lc is a String; Array#intersect? would raise TypeError
145
- show_it = positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }
146
- show_it &&= negative_terms.none? { |q| entry_lc.include?(q) }
147
- if show_it
148
- puts entry
149
- counter += 1
150
- end
151
- end
136
+ def format_ollama_entry(model)
137
+ size = model['size'] ? format_bytes(model['size']) : 'unknown'
138
+ modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
139
+ "- ollama/#{model['name']} (size: #{size}, modified: #{modified})"
140
+ end
152
141
 
153
- puts
154
- puts "#{counter} Ollama model(s) available"
155
- puts
156
- rescue StandardError => e
157
- puts "❌ Error fetching Ollama models: #{e.message}"
158
- end
142
+ # Fetch and parse JSON from a local model server; nil unless HTTP success.
143
+ def fetch_json(uri_string)
144
+ uri = URI(uri_string)
145
+ http = Net::HTTP.new(uri.host, uri.port)
146
+ http.open_timeout = 5
147
+ http.read_timeout = 5
148
+ response = http.request(Net::HTTP::Get.new(uri))
149
+ return nil unless response.is_a?(Net::HTTPSuccess)
150
+
151
+ JSON.parse(response.body)
159
152
  end
160
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
161
153
 
162
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
163
- def show_lms_models(api_base, positive_terms = nil, negative_terms = nil)
164
- positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
154
+ # The entry is a String; Array#intersect? would raise TypeError
155
+ def entry_matches?(entry_lc, positive_terms, negative_terms)
156
+ (positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }) &&
157
+ negative_terms.none? { |q| entry_lc.include?(q) }
158
+ end
165
159
 
166
- begin
167
- uri = URI("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
168
- http = Net::HTTP.new(uri.host, uri.port)
169
- http.open_timeout = 5
170
- http.read_timeout = 5
171
- response = http.request(Net::HTTP::Get.new(uri))
160
+ def print_filtered_entries(entries, positive_terms, negative_terms, summary_label)
161
+ counter = 0
162
+ entries.each do |entry|
163
+ next unless entry_matches?(entry.downcase, positive_terms, negative_terms)
172
164
 
173
- unless response.is_a?(Net::HTTPSuccess)
174
- puts "❌ Cannot connect to LM Studio at #{api_base}"
175
- return
176
- end
165
+ puts entry
166
+ counter += 1
167
+ end
177
168
 
178
- data = JSON.parse(response.body)
179
- models = data['data'] || []
169
+ puts
170
+ puts "#{counter} #{summary_label}"
171
+ puts
172
+ end
180
173
 
181
- if models.empty?
182
- puts "No LM Studio models found"
183
- return
184
- end
174
+ # :reek:TooManyStatements -- sequential terminal report: fetch, connection guards, filtered listing
175
+ def show_lms_models(api_base, positive_terms = nil, negative_terms = nil)
176
+ positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
185
177
 
186
- puts "LM Studio Models (#{api_base}):"
187
- puts "-" * 60
188
-
189
- counter = 0
190
- models.each do |model|
191
- name = model['id']
192
- entry = "- lms/#{name}"
193
- entry_lc = entry.downcase
194
-
195
- # entry_lc is a String; Array#intersect? would raise TypeError
196
- show_it = positive_terms.empty? || positive_terms.any? { |q| entry_lc.include?(q) }
197
- show_it &&= negative_terms.none? { |q| entry_lc.include?(q) }
198
- if show_it
199
- puts entry
200
- counter += 1
201
- end
202
- end
178
+ data = fetch_json("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
179
+ if data.nil?
180
+ puts "❌ Cannot connect to LM Studio at #{api_base}"
181
+ return
182
+ end
203
183
 
204
- puts
205
- puts "#{counter} LM Studio model(s) available"
206
- puts
207
- rescue StandardError => e
208
- puts "❌ Error fetching LM Studio models: #{e.message}"
184
+ models = data['data'] || []
185
+ if models.empty?
186
+ puts "No LM Studio models found"
187
+ return
209
188
  end
189
+
190
+ puts "LM Studio Models (#{api_base}):"
191
+ puts "-" * 60
192
+
193
+ entries = models.map { |model| "- lms/#{model['id']}" }
194
+ print_filtered_entries(entries, positive_terms, negative_terms, "LM Studio model(s) available")
195
+ rescue StandardError => e
196
+ puts "❌ Error fetching LM Studio models: #{e.message}"
210
197
  end
211
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
212
198
 
213
199
  def format_bytes(bytes)
214
200
  units = %w[B KB MB GB TB]
@@ -220,59 +206,53 @@ module AIA
220
206
  "%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
221
207
  end
222
208
 
223
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
209
+ # :reek:TooManyStatements -- sequential terminal report: header, filtered model listing, summary
224
210
  def show_rubyllm_models(positive_terms = nil, negative_terms = nil)
225
211
  positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
226
212
 
227
213
  # expand comma-separated terms passed as a single token
228
- if positive_terms.size == 1
229
- positive_terms = positive_terms.first.split(',')
230
- end
231
-
232
- header = "\nAvailable LLMs"
233
- header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
234
- header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?
214
+ positive_terms = positive_terms.first.split(',') if positive_terms.size == 1
235
215
 
236
- puts header + ':'
216
+ puts rubyllm_header(positive_terms, negative_terms)
237
217
  puts
238
218
 
239
219
  # modality terms (e.g. "text_to_text") trigger capability checks; the rest
240
220
  # are plain substring filters applied to the formatted entry string
241
- q1, q2 = positive_terms.partition { |q| q.include?('_to_') }
221
+ modality_terms, substring_terms = positive_terms.partition { |q| q.include?('_to_') }
242
222
 
243
223
  counter = 0
244
-
245
224
  RubyLLM.models.all.each do |llm|
246
- cw = llm.context_window
247
- caps = llm.capabilities.join(',')
248
- inputs = llm.modalities.input.join(',')
249
- outputs = llm.modalities.output.join(',')
250
- mode = "#{inputs} to #{outputs}"
251
- in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
252
- entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
253
-
254
- if positive_terms.empty? && negative_terms.empty?
255
- counter += 1
256
- puts entry
257
- next
258
- end
225
+ entry = format_rubyllm_entry(llm)
226
+ next unless rubyllm_entry_visible?(llm, entry, modality_terms, substring_terms, negative_terms)
259
227
 
260
- show_it = true
261
- q1.each { |q| show_it &&= llm.modalities.send("#{q}?") }
262
- q2.each { |q| show_it &&= entry.include?(q) }
263
- negative_terms.each { |q| show_it &&= !entry.downcase.include?(q) }
264
-
265
- if show_it
266
- counter += 1
267
- puts entry
268
- end
228
+ counter += 1
229
+ puts entry
269
230
  end
270
231
 
271
232
  puts if counter.positive?
272
233
  puts "#{counter} LLMs matching your query"
273
234
  puts
274
235
  end
275
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
236
+
237
+ def rubyllm_header(positive_terms, negative_terms)
238
+ header = "\nAvailable LLMs"
239
+ header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
240
+ header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?
241
+ header + ':'
242
+ end
243
+
244
+ def format_rubyllm_entry(llm)
245
+ modalities = llm.modalities
246
+ mode = "#{modalities.input.join(',')} to #{modalities.output.join(',')}"
247
+ in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
248
+ "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{llm.context_window} mode: #{mode} caps: #{llm.capabilities.join(',')}"
249
+ end
250
+
251
+ def rubyllm_entry_visible?(llm, entry, modality_terms, substring_terms, negative_terms)
252
+ modality_terms.all? { |q| llm.modalities.send("#{q}?") } &&
253
+ substring_terms.all? { |q| entry.include?(q) } &&
254
+ negative_terms.none? { |q| entry.downcase.include?(q) }
255
+ end
276
256
 
277
257
  def normalized_model_search_terms(positive_terms, negative_terms = nil)
278
258
  return parse_search_terms(Array(positive_terms)) if negative_terms.nil?
@@ -9,12 +9,12 @@ module AIA
9
9
  desc "Show TrakFlow ready tasks and project summary"
10
10
  def tasks(args, context_manager = nil)
11
11
  bridge = TrakFlowBridge.new
12
- return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
12
+ return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
13
13
 
14
14
  if args.first == "summary"
15
- bridge.project_summary || "No summary available."
15
+ bridge.project_summary || report_status("No summary available.")
16
16
  else
17
- bridge.check_ready_tasks || "No ready tasks found."
17
+ bridge.check_ready_tasks || report_status("No ready tasks found.")
18
18
  end
19
19
  end
20
20
  alias tf tasks
@@ -22,23 +22,41 @@ module AIA
22
22
  desc "Create a TrakFlow plan from a description"
23
23
  def plan(args, context_manager = nil)
24
24
  bridge = TrakFlowBridge.new
25
- return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
25
+ return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
26
26
 
27
27
  description = args.join(' ')
28
- return "Usage: /plan <description>" if description.empty?
28
+ return report_error("Usage: /plan <description>") if description.empty?
29
29
 
30
- bridge.create_task(description) || "Failed to create plan."
30
+ bridge.create_task(description) || report_error("Failed to create plan.")
31
31
  end
32
32
 
33
33
  desc "Create a TrakFlow task"
34
34
  def task(args, context_manager = nil)
35
35
  bridge = TrakFlowBridge.new
36
- return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
36
+ return report_error("TrakFlow not available. Run 'tf init' to initialize a project.") unless bridge.available?
37
37
 
38
38
  title = args.join(' ')
39
- return "Usage: /task <title>" if title.empty?
39
+ return report_error("Usage: /task <title>") if title.empty?
40
40
 
41
- bridge.create_task(title) || "Failed to create task."
41
+ bridge.create_task(title) || report_error("Failed to create task.")
42
+ end
43
+
44
+ private
45
+
46
+ # Log and print a directive error, then return nil so the chat loop
47
+ # skips forwarding it to the robot (an error is not conversational output).
48
+ def report_error(msg)
49
+ AIA::LoggerManager.aia_logger.error(msg)
50
+ puts msg
51
+ nil
52
+ end
53
+
54
+ # Same as report_error, but for a non-error empty-result status
55
+ # (e.g. "no ready tasks found") rather than a failure.
56
+ def report_status(msg)
57
+ AIA::LoggerManager.aia_logger.info(msg)
58
+ puts msg
59
+ nil
42
60
  end
43
61
  end
44
62
  end
@@ -6,49 +6,24 @@ require 'word_wrapper'
6
6
  module AIA
7
7
  class UtilityDirectives < Directive
8
8
  desc "List available tools (optional filter by name or description substring)"
9
- # rubocop:disable Metrics/AbcSize
10
9
  def tools(args = [], context_manager = nil)
11
- indent = 4
12
- spaces = " " * indent
13
- width = TTY::Screen.width - indent - 2
14
- filter = args.first&.downcase
15
-
10
+ raw_filter = args.first
16
11
  loaded_tools = Array(AIA.config.loaded_tools) + all_mcp_tools
17
12
 
18
13
  if loaded_tools.empty?
19
14
  puts "No tools are available"
20
15
  else
21
- tools_to_display = loaded_tools
22
-
23
- if filter
24
- tools_to_display = tools_to_display.select do |tool|
25
- name = tool.respond_to?(:name) ? tool.name : tool.class.name
26
- desc = tool.respond_to?(:description) ? tool.description.to_s : ""
27
- "#{name} #{desc}".downcase.include?(filter)
28
- end
29
- end
30
-
16
+ tools_to_display = filter_tools(loaded_tools, raw_filter&.downcase)
31
17
  if tools_to_display.empty?
32
- puts "No tools match the filter: #{args.first}"
18
+ puts "No tools match the filter: #{raw_filter}"
33
19
  else
34
- puts
35
- header = filter ? "Available Tools (filtered by '#{args.first}')" : "Available Tools"
36
- puts header
37
- puts "=" * header.length
38
-
39
- tools_to_display.each do |tool|
40
- name = tool.respond_to?(:name) ? tool.name : tool.class.name
41
- puts "\n#{name}"
42
- puts "-" * name.size
43
- puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
44
- end
20
+ print_tools_report(tools_to_display, raw_filter)
45
21
  end
46
22
  end
47
23
  puts
48
24
 
49
25
  ''
50
26
  end
51
- # rubocop:enable Metrics/AbcSize
52
27
 
53
28
  desc "List loaded plugin basenames"
54
29
  def plugins(args = [], context_manager = nil)
@@ -68,57 +43,22 @@ module AIA
68
43
  end
69
44
 
70
45
  desc "Show MCP server connection status and available tools"
71
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
72
46
  def mcp(args = [], context_manager = nil)
73
47
  connected = AIA.config&.connected_mcp_servers || []
74
48
  failed = AIA.config&.failed_mcp_servers || []
75
- defined_count = AIA::Utility.effective_mcp_server_names.size
76
49
 
77
50
  puts
78
51
  puts "MCP Server Status"
79
52
  puts "================="
80
- puts "Defined: #{defined_count} Connected: #{connected.size} Failed: #{failed.size}"
53
+ puts "Defined: #{AIA::Utility.effective_mcp_server_names.size} " \
54
+ "Connected: #{connected.size} Failed: #{failed.size}"
81
55
  puts
82
56
 
83
- if connected.any?
84
- mcp_tools = all_mcp_tools
85
-
86
- # Group tools by their MCP server name
87
- tools_by_server = {}
88
- connected.each { |name| tools_by_server[name] = [] }
89
-
90
- mcp_tools.each do |tool|
91
- server_name = tool.respond_to?(:mcp) ? tool.mcp : nil
92
- if server_name && tools_by_server.key?(server_name)
93
- tools_by_server[server_name] << tool
94
- end
95
- end
96
-
97
- puts "Connected Servers:"
98
- connected.each do |name|
99
- tools = tools_by_server[name] || []
100
- puts " #{name} (#{tools.size} tools)"
101
- tools.each do |tool|
102
- tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
103
- puts " - #{tool_name}"
104
- end
105
- end
106
- puts
107
- end
108
-
109
- if failed.any?
110
- puts "Failed Servers:"
111
- failed.each do |f|
112
- name = f[:name] || f['name']
113
- error = f[:error] || f['error']
114
- puts " #{name}: #{error}"
115
- end
116
- puts
117
- end
57
+ print_connected_servers(connected) if connected.any?
58
+ print_failed_servers(failed) if failed.any?
118
59
 
119
60
  ''
120
61
  end
121
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
122
62
 
123
63
  desc "Show active robot configuration"
124
64
  def robots(args = [], context_manager = nil)
@@ -152,13 +92,72 @@ module AIA
152
92
 
153
93
  private
154
94
 
95
+ def filter_tools(tools, filter)
96
+ return tools unless filter
97
+
98
+ tools.select do |tool|
99
+ "#{ToolIntrospection.tool_name(tool)} #{ToolIntrospection.tool_description(tool)}"
100
+ .downcase.include?(filter)
101
+ end
102
+ end
103
+
104
+ def print_tools_report(tools_to_display, raw_filter)
105
+ puts
106
+ header = raw_filter ? "Available Tools (filtered by '#{raw_filter}')" : "Available Tools"
107
+ puts header
108
+ puts "=" * header.length
109
+
110
+ indent = 4
111
+ width = TTY::Screen.width - indent - 2
112
+ tools_to_display.each { |tool| print_tool_entry(tool, width, " " * indent) }
113
+ end
114
+
115
+ def print_tool_entry(tool, width, spaces)
116
+ name = ToolIntrospection.tool_name(tool)
117
+ puts "\n#{name}"
118
+ puts "-" * name.size
119
+ puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
120
+ end
121
+
122
+ def print_connected_servers(connected)
123
+ grouped = tools_by_server(connected, all_mcp_tools)
124
+
125
+ puts "Connected Servers:"
126
+ connected.each do |name|
127
+ tools = grouped[name] || []
128
+ puts " #{name} (#{tools.size} tools)"
129
+ tools.each { |tool| puts " - #{ToolIntrospection.tool_name(tool)}" }
130
+ end
131
+ puts
132
+ end
133
+
134
+ # Group MCP tools under the connected server each one came from.
135
+ def tools_by_server(connected, mcp_tools)
136
+ grouped = connected.to_h { |name| [name, []] }
137
+ mcp_tools.each do |tool|
138
+ server_name = tool.respond_to?(:mcp) ? tool.mcp : nil
139
+ grouped[server_name] << tool if server_name && grouped.key?(server_name)
140
+ end
141
+ grouped
142
+ end
143
+
144
+ def print_failed_servers(failed)
145
+ puts "Failed Servers:"
146
+ failed.each do |f|
147
+ puts " #{f[:name] || f['name']}: #{f[:error] || f['error']}"
148
+ end
149
+ puts
150
+ end
151
+
152
+ # :reek:TooManyStatements -- mode classification plus sequential report of every crew member
155
153
  def show_network(network)
154
+ cfg = AIA.config
156
155
  robot_count = network.robot_count
157
- mode = if AIA.config.flags.consensus
156
+ mode = if cfg.flags.consensus
158
157
  "Consensus"
159
- elsif AIA.config.pipeline.length > 1
158
+ elsif cfg.pipeline.length > 1
160
159
  "Pipeline"
161
- elsif AIA.config.models.length > 1
160
+ elsif cfg.models.length > 1
162
161
  "Parallel"
163
162
  else
164
163
  "Crew" # single-model session wrapped as a one-member crew
@@ -185,6 +184,7 @@ module AIA
185
184
  show_robot_detail(bot)
186
185
  end
187
186
 
187
+ # :reek:TooManyStatements -- one labeled line per robot attribute (model, wage, tool counts, role)
188
188
  def show_robot_detail(bot)
189
189
  puts " #{bot.name}"
190
190