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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +5 -1
  3. data/.loki +231 -0
  4. data/.quality/flay_baseline.txt +1 -0
  5. data/.quality/flog_baseline.txt +29 -0
  6. data/.quality/reek_baseline.txt +80 -0
  7. data/.rubocop.yml +116 -0
  8. data/.version +1 -1
  9. data/CHANGELOG.md +259 -50
  10. data/IMPLEMENTATION_PLAN.md +506 -0
  11. data/README.md +266 -238
  12. data/Rakefile +118 -5
  13. data/architecture_review.md +314 -0
  14. data/bin/aia +16 -0
  15. data/docs/AGENTS.md +40 -0
  16. data/docs/advanced-prompting.md +67 -3
  17. data/docs/cli-reference.md +312 -56
  18. data/docs/configuration.md +130 -19
  19. data/docs/contributing.md +56 -2
  20. data/docs/directives-reference.md +593 -78
  21. data/docs/faq.md +85 -3
  22. data/docs/guides/available-models.md +1 -1
  23. data/docs/guides/basic-usage.md +6 -6
  24. data/docs/guides/chat.md +40 -16
  25. data/docs/guides/crew.md +239 -0
  26. data/docs/guides/executable-prompts.md +1 -1
  27. data/docs/guides/index.md +1 -0
  28. data/docs/guides/models.md +15 -0
  29. data/docs/index.md +29 -2
  30. data/docs/installation.md +44 -17
  31. data/docs/mcp-integration.md +40 -0
  32. data/docs/prompt_management.md +85 -86
  33. data/docs/security.md +47 -0
  34. data/docs/special_projects_guide.md +386 -0
  35. data/docs/tools-and-mcp-examples.md +23 -0
  36. data/docs/workflows-and-pipelines.md +84 -7
  37. data/examples/.gitignore +1 -0
  38. data/examples/00_setup_aia.sh +27 -44
  39. data/examples/11_multi_model.sh +4 -14
  40. data/examples/12_token_usage.sh +3 -12
  41. data/examples/18_tools.sh +10 -2
  42. data/examples/22_chat_mode.sh +0 -10
  43. data/examples/23_verify.sh +139 -0
  44. data/examples/24_decompose.sh +139 -0
  45. data/examples/25_spawn.sh +139 -0
  46. data/examples/26_debate.sh +97 -0
  47. data/examples/27_mention_routing.sh +157 -0
  48. data/examples/28_model_switching.sh +106 -0
  49. data/examples/29_agent_harness.sh +177 -0
  50. data/examples/README.md +65 -0
  51. data/examples/advanced_multi_robot_capabilities_without_examples.md +106 -0
  52. data/examples/aia_config.yml +1 -1
  53. data/examples/aia_config_orchestrator.yml +45 -0
  54. data/examples/common.sh +18 -6
  55. data/examples/context/tech_stack.md +2 -2
  56. data/examples/prompts_dir/roles/orchestrator.md +21 -0
  57. data/examples/requirements/sinatra_taskflow_app.md +139 -0
  58. data/examples/rules/01_classify_ruby.rb +16 -0
  59. data/examples/rules/02_prefer_claude_for_code.rb +19 -0
  60. data/examples/rules/03_gate_prompt_length.rb +19 -0
  61. data/examples/rules/04_tool_selection.rb +41 -0
  62. data/examples/rules/README.md +30 -0
  63. data/examples/run_all.sh +48 -15
  64. data/examples/tools/word_count_tool.rb +1 -1
  65. data/lib/AGENTS.md +57 -0
  66. data/lib/aia/chat_loop.rb +304 -164
  67. data/lib/aia/config/cli_parser.rb +174 -111
  68. data/lib/aia/config/defaults.yml +62 -33
  69. data/lib/aia/config/mcp_parser.rb +39 -46
  70. data/lib/aia/config/model_spec.rb +34 -2
  71. data/lib/aia/config/validator.rb +108 -142
  72. data/lib/aia/config.rb +110 -145
  73. data/lib/aia/content_extractor.rb +153 -0
  74. data/lib/aia/cost_calculator.rb +38 -0
  75. data/lib/aia/crew.rb +164 -0
  76. data/lib/aia/debate_handler.rb +166 -0
  77. data/lib/aia/delegate_handler.rb +112 -0
  78. data/lib/aia/directive.rb +33 -18
  79. data/lib/aia/directive_processor.rb +16 -7
  80. data/lib/aia/directives/configuration_directives.rb +160 -20
  81. data/lib/aia/directives/context_directives.rb +38 -26
  82. data/lib/aia/directives/execution_directives.rb +136 -4
  83. data/lib/aia/directives/model_directives.rb +76 -34
  84. data/lib/aia/directives/trakflow_directives.rb +44 -0
  85. data/lib/aia/directives/utility_directives.rb +203 -6
  86. data/lib/aia/directives/web_and_file_directives.rb +96 -60
  87. data/lib/aia/errors.rb +15 -0
  88. data/lib/aia/fact_asserter.rb +27 -0
  89. data/lib/aia/fzf.rb +9 -31
  90. data/lib/aia/handler_context.rb +17 -0
  91. data/lib/aia/handler_protocol.rb +19 -0
  92. data/lib/aia/history_transfer.rb +55 -0
  93. data/lib/aia/input_collector.rb +3 -3
  94. data/lib/aia/layered_orchestrator.rb +448 -0
  95. data/lib/aia/logger.rb +24 -4
  96. data/lib/aia/mcp_config_normalizer.rb +35 -0
  97. data/lib/aia/mcp_connection_manager.rb +305 -0
  98. data/lib/aia/mcp_discovery.rb +44 -0
  99. data/lib/aia/mcp_grouper.rb +33 -0
  100. data/lib/aia/mcp_utility.rb +57 -0
  101. data/lib/aia/mention_router.rb +260 -0
  102. data/lib/aia/model_alias_registry.rb +97 -0
  103. data/lib/aia/model_switch_handler.rb +100 -0
  104. data/lib/aia/network_builder.rb +155 -0
  105. data/lib/aia/network_memory_manager.rb +55 -0
  106. data/lib/aia/patches/ruby_llm_streaming_error.rb +43 -0
  107. data/lib/aia/patches/ruby_llm_tool_error.rb +96 -0
  108. data/lib/aia/pipeline_orchestrator.rb +262 -0
  109. data/lib/aia/plugin_loader.rb +170 -0
  110. data/lib/aia/plugin_monitor.rb +208 -0
  111. data/lib/aia/prompt_decomposer.rb +157 -0
  112. data/lib/aia/prompt_handler.rb +19 -39
  113. data/lib/aia/robot_builder.rb +51 -0
  114. data/lib/aia/robot_factory.rb +334 -0
  115. data/lib/aia/robot_namer.rb +116 -0
  116. data/lib/aia/session.rb +83 -17
  117. data/lib/aia/session_tracker.rb +209 -0
  118. data/lib/aia/similarity_scorer.rb +39 -0
  119. data/lib/aia/skill_utils.rb +105 -1
  120. data/lib/aia/spawn_handler.rb +129 -0
  121. data/lib/aia/spawn_spec_parser.rb +65 -0
  122. data/lib/aia/special_mode_handler.rb +302 -0
  123. data/lib/aia/startup_coordinator.rb +150 -0
  124. data/lib/aia/streaming_runner.rb +169 -0
  125. data/lib/aia/system_prompt_assembler.rb +88 -0
  126. data/lib/aia/task_coordinator.rb +202 -0
  127. data/lib/aia/task_decomposer.rb +57 -0
  128. data/lib/aia/task_executor.rb +51 -0
  129. data/lib/aia/tfidf_math.rb +27 -0
  130. data/lib/aia/tool_filter/tfidf.rb +116 -0
  131. data/lib/aia/tool_filter/wordnet_expander.rb +127 -0
  132. data/lib/aia/tool_filter.rb +82 -0
  133. data/lib/aia/tool_filter_registry.rb +30 -0
  134. data/lib/aia/tool_filter_strategy.rb +143 -0
  135. data/lib/aia/tool_loader.rb +210 -0
  136. data/lib/aia/tool_utility.rb +30 -0
  137. data/lib/aia/tools/delegate_to_foreman_tool.rb +70 -0
  138. data/lib/aia/tools/recruit_robot_tool.rb +60 -0
  139. data/lib/aia/tools/reskill_robot_tool.rb +44 -0
  140. data/lib/aia/tools/task_board_tool.rb +114 -0
  141. data/lib/aia/trakflow_bridge.rb +173 -0
  142. data/lib/aia/turn_state.rb +94 -0
  143. data/lib/aia/ui_presenter.rb +166 -198
  144. data/lib/aia/utility.rb +134 -87
  145. data/lib/aia/{history_manager.rb → variable_input_collector.rb} +8 -9
  146. data/lib/aia/verification_network.rb +58 -0
  147. data/lib/aia.rb +108 -63
  148. data/mkdocs.yml +1 -0
  149. metadata +179 -56
  150. data/justfile +0 -215
  151. data/lib/aia/adapter/chat_execution.rb +0 -242
  152. data/lib/aia/adapter/error_handler.rb +0 -68
  153. data/lib/aia/adapter/gem_activator.rb +0 -57
  154. data/lib/aia/adapter/mcp_connector.rb +0 -274
  155. data/lib/aia/adapter/modality_handlers.rb +0 -167
  156. data/lib/aia/adapter/model_registry.rb +0 -81
  157. data/lib/aia/adapter/multi_model_chat.rb +0 -218
  158. data/lib/aia/adapter/provider_configurator.rb +0 -59
  159. data/lib/aia/adapter/tool_filter.rb +0 -85
  160. data/lib/aia/adapter/tool_loader.rb +0 -90
  161. data/lib/aia/chat_processor_service.rb +0 -178
  162. data/lib/aia/prompt_pipeline.rb +0 -183
  163. data/lib/aia/ruby_llm_adapter.rb +0 -95
  164. data/lib/extensions/openstruct_merge.rb +0 -48
  165. data/lib/extensions/ruby_llm/.irbrc +0 -56
  166. data/lib/extensions/ruby_llm/modalities.rb +0 -36
  167. data/lib/extensions/ruby_llm/provider_fix.rb +0 -79
  168. data/lib/refinements/string.rb +0 -16
  169. data/main.just +0 -76
@@ -4,14 +4,14 @@ module AIA
4
4
  class ModelDirectives < Directive
5
5
  desc "List all available AI models"
6
6
  def available_models(args = nil, context_manager = nil)
7
- positive_terms, negative_terms = parse_search_terms(Array(args))
8
-
9
7
  current_models = AIA.config.models
10
8
 
11
9
  model_names = current_models.map do |m|
12
10
  m.respond_to?(:name) ? m.name : m.to_s
13
11
  end
14
12
 
13
+ positive_terms, negative_terms = parse_search_terms(Array(args))
14
+
15
15
  using_local_provider = model_names.any? { |m| m.start_with?('ollama/', 'lms/') }
16
16
 
17
17
  if using_local_provider
@@ -22,13 +22,14 @@ module AIA
22
22
 
23
23
  ""
24
24
  end
25
- alias_method :am, :available_models
26
- alias_method :available, :available_models
27
- alias_method :models, :available_models
28
- alias_method :all_models, :available_models
29
- alias_method :llms, :available_models
25
+ alias am available_models
26
+ alias available available_models
27
+ alias models available_models
28
+ alias all_models available_models
29
+ alias llms available_models
30
30
 
31
31
  desc "Compare responses from multiple models"
32
+ # rubocop:disable Metrics/MethodLength
32
33
  def compare(args, context_manager = nil)
33
34
  return 'Error: No prompt provided for comparison' if args.empty?
34
35
 
@@ -73,19 +74,22 @@ module AIA
73
74
  end
74
75
  end
75
76
 
76
- puts '\n' + '=' * 80
77
+ puts "\n" + ('=' * 80)
77
78
  puts "\nComparison complete!"
78
79
 
79
80
  ''
80
81
  end
81
- alias_method :cmp, :compare
82
+ # rubocop:enable Metrics/MethodLength
83
+ alias cmp compare
82
84
 
83
85
  # --- helpers (no desc → not registered) ---
84
86
 
85
- def show_local_models(current_models, positive_terms, negative_terms)
87
+ def show_local_models(current_models, positive_terms = nil, negative_terms = nil)
86
88
  require 'net/http'
87
89
  require 'json'
88
90
 
91
+ positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
92
+
89
93
  puts "\nLocal LLM Models:"
90
94
  puts
91
95
 
@@ -101,10 +105,16 @@ module AIA
101
105
  end
102
106
  end
103
107
 
104
- def show_ollama_models(api_base, positive_terms, negative_terms)
108
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
109
+ def show_ollama_models(api_base, positive_terms = nil, negative_terms = nil)
110
+ positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
111
+
105
112
  begin
106
113
  uri = URI("#{api_base}/api/tags")
107
- response = Net::HTTP.get_response(uri)
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))
108
118
 
109
119
  unless response.is_a?(Net::HTTPSuccess)
110
120
  puts "❌ Cannot connect to Ollama at #{api_base}"
@@ -129,11 +139,11 @@ module AIA
129
139
  modified = model['modified_at'] ? Time.parse(model['modified_at']).strftime('%Y-%m-%d') : 'unknown'
130
140
 
131
141
  entry = "- ollama/#{name} (size: #{size}, modified: #{modified})"
132
- entry_down = entry.downcase
133
-
134
- show_it = positive_terms.empty? || positive_terms.any? { |q| entry_down.include?(q) }
135
- show_it &&= negative_terms.none? { |q| entry_down.include?(q) }
142
+ entry_lc = entry.downcase
136
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) }
137
147
  if show_it
138
148
  puts entry
139
149
  counter += 1
@@ -147,11 +157,18 @@ module AIA
147
157
  puts "❌ Error fetching Ollama models: #{e.message}"
148
158
  end
149
159
  end
160
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
161
+
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)
150
165
 
151
- def show_lms_models(api_base, positive_terms, negative_terms)
152
166
  begin
153
167
  uri = URI("#{api_base.gsub(%r{/v1/?$}, '')}/v1/models")
154
- response = Net::HTTP.get_response(uri)
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))
155
172
 
156
173
  unless response.is_a?(Net::HTTPSuccess)
157
174
  puts "❌ Cannot connect to LM Studio at #{api_base}"
@@ -173,11 +190,11 @@ module AIA
173
190
  models.each do |model|
174
191
  name = model['id']
175
192
  entry = "- lms/#{name}"
176
- entry_down = entry.downcase
177
-
178
- show_it = positive_terms.empty? || positive_terms.any? { |q| entry_down.include?(q) }
179
- show_it &&= negative_terms.none? { |q| entry_down.include?(q) }
193
+ entry_lc = entry.downcase
180
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) }
181
198
  if show_it
182
199
  puts entry
183
200
  counter += 1
@@ -191,28 +208,38 @@ module AIA
191
208
  puts "❌ Error fetching LM Studio models: #{e.message}"
192
209
  end
193
210
  end
211
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
194
212
 
195
213
  def format_bytes(bytes)
196
- units = ['B', 'KB', 'MB', 'GB', 'TB']
214
+ units = %w[B KB MB GB TB]
197
215
  return "0 B" if bytes.zero?
198
216
 
199
217
  exp = (Math.log(bytes) / Math.log(1024)).to_i
200
218
  exp = [exp, units.length - 1].min
201
219
 
202
- "%.1f %s" % [bytes.to_f / (1024 ** exp), units[exp]]
220
+ "%.1f %s" % [bytes.to_f / (1024**exp), units[exp]]
203
221
  end
204
222
 
205
- def show_rubyllm_models(positive_terms, negative_terms)
206
- modality_terms = positive_terms.select { |q| q.include?('_to_') }
207
- text_terms = positive_terms.reject { |q| q.include?('_to_') }
223
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
224
+ def show_rubyllm_models(positive_terms = nil, negative_terms = nil)
225
+ positive_terms, negative_terms = normalized_model_search_terms(positive_terms, negative_terms)
226
+
227
+ # expand comma-separated terms passed as a single token
228
+ if positive_terms.size == 1
229
+ positive_terms = positive_terms.first.split(',')
230
+ end
208
231
 
209
232
  header = "\nAvailable LLMs"
210
- header += " for #{positive_terms.join(' and ')}" unless positive_terms.empty?
211
- header += " excluding: #{negative_terms.join(', ')}" unless negative_terms.empty?
233
+ header += " for #{positive_terms.join(' and ')}" if positive_terms.any?
234
+ header += " (excluding: #{negative_terms.join(', ')})" if negative_terms.any?
212
235
 
213
236
  puts header + ':'
214
237
  puts
215
238
 
239
+ # modality terms (e.g. "text_to_text") trigger capability checks; the rest
240
+ # are plain substring filters applied to the formatted entry string
241
+ q1, q2 = positive_terms.partition { |q| q.include?('_to_') }
242
+
216
243
  counter = 0
217
244
 
218
245
  RubyLLM.models.all.each do |llm|
@@ -223,12 +250,17 @@ module AIA
223
250
  mode = "#{inputs} to #{outputs}"
224
251
  in_1m = llm.pricing.text_tokens.standard.to_h[:input_per_million]
225
252
  entry = "- #{llm.id} (#{llm.provider}) in: $#{in_1m} cw: #{cw} mode: #{mode} caps: #{caps}"
226
- entry_down = entry.downcase
253
+
254
+ if positive_terms.empty? && negative_terms.empty?
255
+ counter += 1
256
+ puts entry
257
+ next
258
+ end
227
259
 
228
260
  show_it = true
229
- modality_terms.each { |q| show_it &&= llm.modalities.send("#{q}?") }
230
- text_terms.each { |q| show_it &&= entry_down.include?(q) }
231
- negative_terms.each { |q| show_it &&= !entry_down.include?(q) }
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) }
232
264
 
233
265
  if show_it
234
266
  counter += 1
@@ -236,9 +268,19 @@ module AIA
236
268
  end
237
269
  end
238
270
 
239
- puts if counter > 0
271
+ puts if counter.positive?
240
272
  puts "#{counter} LLMs matching your query"
241
273
  puts
242
274
  end
275
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
276
+
277
+ def normalized_model_search_terms(positive_terms, negative_terms = nil)
278
+ return parse_search_terms(Array(positive_terms)) if negative_terms.nil?
279
+
280
+ [
281
+ Array(positive_terms).compact.map { |term| term.to_s.downcase },
282
+ Array(negative_terms).compact.map { |term| term.to_s.downcase }
283
+ ]
284
+ end
243
285
  end
244
286
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/aia/directives/trakflow_directives.rb
4
+ #
5
+ # Chat directives for interacting with TrakFlow task tracking.
6
+
7
+ module AIA
8
+ class TrakFlowDirectives < Directive
9
+ desc "Show TrakFlow ready tasks and project summary"
10
+ def tasks(args, context_manager = nil)
11
+ bridge = TrakFlowBridge.new
12
+ return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
13
+
14
+ if args.first == "summary"
15
+ bridge.project_summary || "No summary available."
16
+ else
17
+ bridge.check_ready_tasks || "No ready tasks found."
18
+ end
19
+ end
20
+ alias tf tasks
21
+
22
+ desc "Create a TrakFlow plan from a description"
23
+ def plan(args, context_manager = nil)
24
+ bridge = TrakFlowBridge.new
25
+ return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
26
+
27
+ description = args.join(' ')
28
+ return "Usage: /plan <description>" if description.empty?
29
+
30
+ bridge.create_task(description) || "Failed to create plan."
31
+ end
32
+
33
+ desc "Create a TrakFlow task"
34
+ def task(args, context_manager = nil)
35
+ bridge = TrakFlowBridge.new
36
+ return "TrakFlow not available. Run 'tf init' to initialize a project." unless bridge.available?
37
+
38
+ title = args.join(' ')
39
+ return "Usage: /task <title>" if title.empty?
40
+
41
+ bridge.create_task(title) || "Failed to create task."
42
+ end
43
+ end
44
+ end
@@ -5,14 +5,16 @@ require 'word_wrapper'
5
5
 
6
6
  module AIA
7
7
  class UtilityDirectives < Directive
8
- desc "List available tools (optional filter by name substring)"
8
+ desc "List available tools (optional filter by name or description substring)"
9
+ # rubocop:disable Metrics/AbcSize
9
10
  def tools(args = [], context_manager = nil)
10
11
  indent = 4
11
12
  spaces = " " * indent
12
13
  width = TTY::Screen.width - indent - 2
13
14
  filter = args.first&.downcase
14
15
 
15
- loaded_tools = AIA.config.loaded_tools || []
16
+ loaded_tools = Array(AIA.config.loaded_tools) + all_mcp_tools
17
+
16
18
  if loaded_tools.empty?
17
19
  puts "No tools are available"
18
20
  else
@@ -21,7 +23,8 @@ module AIA
21
23
  if filter
22
24
  tools_to_display = tools_to_display.select do |tool|
23
25
  name = tool.respond_to?(:name) ? tool.name : tool.class.name
24
- name.downcase.include?(filter)
26
+ desc = tool.respond_to?(:description) ? tool.description.to_s : ""
27
+ "#{name} #{desc}".downcase.include?(filter)
25
28
  end
26
29
  end
27
30
 
@@ -45,10 +48,95 @@ module AIA
45
48
 
46
49
  ''
47
50
  end
51
+ # rubocop:enable Metrics/AbcSize
52
+
53
+ desc "List loaded plugin basenames"
54
+ def plugins(args = [], context_manager = nil)
55
+ loaded_plugins = Array(AIA.config&.loaded_plugins)
56
+
57
+ if loaded_plugins.empty?
58
+ puts "No plugins are loaded"
59
+ else
60
+ puts
61
+ puts "Loaded Plugins"
62
+ puts "=============="
63
+ loaded_plugins.each { |name| puts name }
64
+ end
65
+ puts
66
+
67
+ ''
68
+ end
69
+
70
+ desc "Show MCP server connection status and available tools"
71
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
72
+ def mcp(args = [], context_manager = nil)
73
+ connected = AIA.config&.connected_mcp_servers || []
74
+ failed = AIA.config&.failed_mcp_servers || []
75
+ defined_count = AIA::Utility.effective_mcp_server_names.size
76
+
77
+ puts
78
+ puts "MCP Server Status"
79
+ puts "================="
80
+ puts "Defined: #{defined_count} Connected: #{connected.size} Failed: #{failed.size}"
81
+ puts
82
+
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
118
+
119
+ ''
120
+ end
121
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
122
+
123
+ desc "Show active robot configuration"
124
+ def robots(args = [], context_manager = nil)
125
+ client = AIA.client
126
+ unless client
127
+ puts "No active robots"
128
+ return ''
129
+ end
130
+
131
+ puts
132
+ if client.network?
133
+ show_network(client)
134
+ else
135
+ show_single_robot(client)
136
+ end
137
+ puts
48
138
 
49
- desc "Add instruction for concise responses"
50
- def terse(args, context_manager = nil)
51
- "" # DEPRECATED: terse mode has been removed
139
+ ''
52
140
  end
53
141
 
54
142
  desc "Display ASCII robot art"
@@ -61,5 +149,114 @@ module AIA
61
149
  def help(args = nil, context_manager = nil)
62
150
  AIA::Directive.help
63
151
  end
152
+
153
+ private
154
+
155
+ def show_network(network)
156
+ robot_count = network.robot_count
157
+ mode = if AIA.config.flags.consensus
158
+ "Consensus"
159
+ elsif AIA.config.pipeline.length > 1
160
+ "Pipeline"
161
+ elsif AIA.config.models.length > 1
162
+ "Parallel"
163
+ else
164
+ "Crew" # single-model session wrapped as a one-member crew
165
+ end
166
+
167
+ header = "Active Robots"
168
+ puts header
169
+ puts "=" * header.length
170
+ noun = robot_count == 1 ? "robot" : "robots"
171
+ puts "Mode: #{mode} (#{robot_count} #{noun})"
172
+
173
+ network.crew.each do |bot|
174
+ puts
175
+ show_robot_detail(bot)
176
+ end
177
+ end
178
+
179
+ def show_single_robot(bot)
180
+ header = "Active Robot"
181
+ puts header
182
+ puts "=" * header.length
183
+ puts "Mode: Single"
184
+ puts
185
+ show_robot_detail(bot)
186
+ end
187
+
188
+ def show_robot_detail(bot)
189
+ puts " #{bot.name}"
190
+
191
+ model_name = bot.model || 'unknown'
192
+ provider = bot.respond_to?(:provider) ? bot.provider : nil
193
+ provider ||= chat_provider(bot)
194
+ model_line = provider ? "#{model_name} (#{provider})" : model_name
195
+ puts " Model: #{model_line}"
196
+ puts " Wage: #{wage_for(model_name)}"
197
+
198
+ local_count = Array(bot.local_tools).size
199
+ mcp_count = Array(bot.mcp_tools).size
200
+ total = local_count + mcp_count
201
+ tool_parts = []
202
+ tool_parts << "#{local_count} local" if local_count.positive?
203
+ tool_parts << "#{mcp_count} mcp" if mcp_count.positive?
204
+ puts " Tools: #{total} (#{tool_parts.join(', ')})" if total.positive?
205
+ puts " Tools: none" if total.zero?
206
+
207
+ role = role_for(bot)
208
+ puts " Role: #{role}"
209
+ end
210
+
211
+ # Cost per 1K tokens (price_per_million * 1000 / 1_000_000)
212
+ def wage_for(model_name)
213
+ model_info = RubyLLM::Models.find(model_name)
214
+ input = model_info&.input_price_per_million
215
+ output = model_info&.output_price_per_million
216
+ return 'N/A' unless input && output
217
+
218
+ in_per_k = input * 1000.0 / 1_000_000
219
+ out_per_k = output * 1000.0 / 1_000_000
220
+ "$#{'%.4f' % in_per_k} in / $#{'%.4f' % out_per_k} out per 1K tokens"
221
+ rescue StandardError
222
+ 'N/A'
223
+ end
224
+
225
+ def chat_provider(bot)
226
+ bot.chat_provider
227
+ rescue StandardError
228
+ nil
229
+ end
230
+
231
+ def role_for(bot)
232
+ spec = AIA.config.models.find { |s| s.name == bot.model }
233
+ if spec&.role?
234
+ spec.role
235
+ else
236
+ "(default)"
237
+ end
238
+ end
239
+
240
+ # Retrieve MCP tools from robot or first robot in a Network.
241
+ # Falls back to RubyLLM::MCP.clients for tools registered by shared_tools
242
+ # or other gems that use RubyLLM::MCP.add_client directly.
243
+ def all_mcp_tools
244
+ robot = AIA.client
245
+ if robot
246
+ if robot.respond_to?(:mcp_tools) && robot.mcp_tools&.any?
247
+ return Array(robot.mcp_tools)
248
+ end
249
+
250
+ first_robot = robot.chief
251
+ if first_robot.respond_to?(:mcp_tools) && first_robot.mcp_tools&.any?
252
+ return Array(first_robot.mcp_tools)
253
+ end
254
+ end
255
+
256
+ # Fall back to RubyLLM::MCP clients (shared_tools and other gems
257
+ # that register MCP servers via RubyLLM::MCP.add_client)
258
+ return [] unless defined?(RubyLLM::MCP)
259
+ RubyLLM::MCP.clients.values.flat_map(&:tools)
260
+ end
64
261
  end
65
262
  end