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
data/aia.gemspec ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/aia/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'aia'
7
+ spec.version = AIA::VERSION
8
+ spec.authors = ['Dewayne VanHoozer']
9
+ spec.email = ['dvanhoozer@gmail.com']
10
+
11
+ spec.summary = 'Multi-model AI CLI with dynamic prompts, consensus responses, shell & Ruby integration, and seamless chat workflows.'
12
+ spec.description = <<~DESC
13
+ AIA is a powerful CLI console application that brings multi-model AI capabilities to your command line, supporting 20+ providers including OpenAI, Anthropic, and Google. Built on robot_lab for robust robot orchestration, AIA v2 provides a thin CLI shell over a rich execution engine. Run multiple AI models simultaneously for comparison, get consensus responses from collaborative AI teams, or compare individual outputs side-by-side. With dynamic prompt management, embedded directives, shell and Ruby integration, interactive chats, and comprehensive history tracking, AIA transforms how you interact with AI.
14
+ DESC
15
+
16
+ spec.homepage = 'https://github.com/MadBomber/aia'
17
+ spec.license = 'MIT'
18
+
19
+ spec.required_ruby_version = '>= 4.0.0'
20
+
21
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = spec.homepage
25
+ spec.metadata['changelog_uri'] = spec.homepage
26
+ spec.metadata['rubygems_mfa_required'] = 'true'
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
30
+ spec.files = Dir.chdir(__dir__) do
31
+ `git ls-files -z`.split("\x0").reject do |f|
32
+ (File.expand_path(f) == __FILE__) ||
33
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git Gemfile])
34
+ end + ['.version']
35
+ end
36
+
37
+ spec.bindir = 'bin'
38
+ spec.executables = %w[aia]
39
+ spec.require_paths = %w[lib]
40
+
41
+ # Pin bigdecimal to 4.x — Ruby 4 ships it as a default gem, but some transitive
42
+ # deps declare >= 3.0 which can cause RubyGems to activate 3.x first.
43
+ spec.add_dependency 'bigdecimal', '>= 4.0'
44
+
45
+ # Core dependencies
46
+ spec.add_dependency 'robot_lab', '~> 0.2' # Execution engine: robots, networks, tools, MCP, memory
47
+ spec.add_dependency 'myway_config' # AIA-specific config (CLI settings, prompts, UI)
48
+ spec.add_dependency 'prompt_manager', '~> 1.0.2' # Prompt parsing/directive DSL
49
+ spec.add_dependency 'lumberjack', '~> 2.1' # Structured logging, 3 loggers (aia, llm, mcp); 2.x API (LogFile shift_age/autoflush)
50
+ spec.add_dependency 'activesupport' # Required by robot_lab (missing from its gemspec)
51
+ spec.add_dependency 'simple_flow'
52
+ spec.add_dependency 'trak_flow'
53
+
54
+ # CLI & UI
55
+ spec.add_dependency 'reline' # Interactive chat input with history
56
+ spec.add_dependency 'tty-screen' # Terminal width detection
57
+ spec.add_dependency 'tty-spinner' # Loading animation and concurrent MCP connection spinners
58
+ spec.add_dependency 'tty-table', '~> 0.12' # Adaptive terminal-width table rendering for metrics
59
+ spec.add_dependency 'classifier', '~> 2.3' # TF-IDF similarity (Option A tool filtering)
60
+ spec.add_dependency 'word_wrapper' # Terminal text wrapping for tool listings
61
+ spec.add_dependency 'amazing_print' # Config dump formatting (--dump, /config)
62
+ spec.add_dependency 'clipboard' # System clipboard access (/paste directive)
63
+
64
+ # Utilities
65
+ spec.add_dependency 'faraday' # HTTP client for /webpage directive
66
+ spec.add_dependency 'listen' # File-watch for live plugin reload (polling fallback if absent)
67
+ spec.add_dependency 'shellwords' # Shell escaping
68
+
69
+ spec.add_development_dependency 'debug_me'
70
+ spec.add_development_dependency 'minitest'
71
+ spec.add_development_dependency 'minitest-reporters'
72
+ spec.add_development_dependency 'mocha'
73
+ spec.add_development_dependency 'rake'
74
+ spec.add_development_dependency 'simplecov'
75
+ spec.add_development_dependency 'simplecov_lcov_formatter'
76
+ spec.add_development_dependency 'tocer'
77
+ spec.add_development_dependency 'webmock'
78
+
79
+ spec.post_install_message = <<~MSG
80
+
81
+ ╔══════════════════════════════════════════════════════════════╗
82
+ ║ AIA — AI Assistant v2.0 ║
83
+ ║ ║
84
+ ║ v2 is powered by robot_lab for robust orchestration ║
85
+ ║ Full CLI backward compatibility with v1 ║
86
+ ╚══════════════════════════════════════════════════════════════╝
87
+
88
+ Get started: aia --help
89
+ Full docs: https://madbomber.github.io/aia
90
+
91
+ MSG
92
+ end
data/config/aia.yml ADDED
@@ -0,0 +1,13 @@
1
+ logger:
2
+ aia:
3
+ file: ~/.config/aia/logs/aia.log
4
+ level: info
5
+ flush: true
6
+ llm:
7
+ file: ~/.config/aia/logs/llm.log
8
+ level: info
9
+ flush: true
10
+ mcp:
11
+ file: ~/.config/aia/logs/mcp.log
12
+ level: info
13
+ flush: true
data/lib/aia/chat_loop.rb CHANGED
@@ -11,8 +11,11 @@ require "reline"
11
11
  require "pm"
12
12
 
13
13
  module AIA
14
+ # :reek:TooManyInstanceVariables -- interactive-loop hub wires robot, presenter, tracker, routers, and handlers together by design
15
+ # :reek:TooManyMethods -- one private helper per loop concern (context, metrics, speech, history)
14
16
  class ChatLoop
15
17
  include ContentExtractor
18
+ include Speech
16
19
 
17
20
  def initialize(robot, ui_presenter, directive_processor,
18
21
  session_tracker: nil, alias_registry: nil, filters: {})
@@ -42,6 +45,7 @@ module AIA
42
45
  end
43
46
 
44
47
  # Start the interactive chat session
48
+ # :reek:BooleanParameter -- lets Session skip re-sending context files after a pipeline; two entry methods would duplicate rescue/ensure
45
49
  def start(skip_context_files: false)
46
50
  setup_session
47
51
  process_initial_context(skip_context_files)
@@ -62,9 +66,10 @@ module AIA
62
66
  end
63
67
 
64
68
  def process_initial_context(skip_context_files)
65
- return if skip_context_files || !AIA.config.context_files || AIA.config.context_files.empty?
69
+ files = AIA.config.context_files
70
+ return if skip_context_files || !files || files.empty?
66
71
 
67
- context = AIA.config.context_files.map do |file|
72
+ context = files.map do |file|
68
73
  File.read(file) rescue "Error reading file: #{file}"
69
74
  end.join("\n\n")
70
75
 
@@ -262,6 +267,7 @@ module AIA
262
267
  # Each robot_result.duration holds the elapsed seconds.
263
268
  # Similarity scores compare each model's response text against the
264
269
  # first model using TF-IDF cosine similarity.
270
+ # :reek:TooManyStatements -- one pass builds paired metric and similarity arrays; splitting hides the pairing
265
271
  def display_network_metrics(flow_result)
266
272
  metrics_list = []
267
273
  response_texts = []
@@ -337,55 +343,5 @@ module AIA
337
343
 
338
344
  File.open(out_file, "a") { |f| f.puts "\nYou: #{input}" }
339
345
  end
340
-
341
- def speak(content)
342
- return unless AIA.speak?
343
-
344
- audio = AIA.config.audio
345
- command = audio.speak_command || 'say'
346
- env = {}
347
- env['SPEECH_MODEL'] = audio.speech_model if audio.speech_model
348
-
349
- if command == 'say'
350
- # Local TTS: say converts and plays in one step
351
- run_with_spinner("Speaking...") do
352
- if audio.voice && !audio.voice.to_s.strip.empty?
353
- system(env, command, '-v', audio.voice, content.to_s)
354
- else
355
- system(env, command, content.to_s)
356
- end
357
- end
358
- else
359
- # Custom TTS script: stage 2 = convert text → audio file,
360
- # stage 3 = play the file. AIA passes the output path as $2.
361
- require 'tempfile'
362
- tmpfile = Tempfile.new(['aia-tts-', '.mp3'])
363
- tmpfile.close
364
- begin
365
- run_with_spinner("Converting to audio...") do
366
- system(env, command, content.to_s, tmpfile.path)
367
- end
368
- if File.size?(tmpfile.path)
369
- run_with_spinner("Speaking...") do
370
- system('afplay', tmpfile.path)
371
- end
372
- end
373
- ensure
374
- tmpfile.unlink
375
- end
376
- end
377
- rescue StandardError => e
378
- $stderr.puts "Warning: Speech failed: #{e.message}"
379
- end
380
-
381
- def run_with_spinner(message)
382
- spinner = TTY::Spinner.new("[:spinner] #{message}", format: :bouncing_ball, output: $stderr)
383
- spinner.auto_spin
384
- begin
385
- yield
386
- ensure
387
- spinner.stop
388
- end
389
- end
390
346
  end
391
347
  end
@@ -56,6 +56,7 @@ module AIA
56
56
  "aia [options] --chat [PROMPT_ID] [CONTEXT_FILE]*"
57
57
  end
58
58
 
59
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
59
60
  def setup_mode_options(opts, options)
60
61
  opts.separator "\nMode Options:"
61
62
 
@@ -96,6 +97,7 @@ module AIA
96
97
  end
97
98
  end
98
99
 
100
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
99
101
  def setup_model_options(opts, options)
100
102
  opts.separator "\nModel Options:"
101
103
 
@@ -127,6 +129,7 @@ module AIA
127
129
  end
128
130
  end
129
131
 
132
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
130
133
  def setup_file_options(opts, options)
131
134
  opts.separator "\nFile & Output Options:"
132
135
 
@@ -157,6 +160,8 @@ module AIA
157
160
  end
158
161
  end
159
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
160
165
  def setup_prompt_options(opts, options)
161
166
  opts.separator "\nPrompt Options:"
162
167
 
@@ -228,6 +233,7 @@ module AIA
228
233
  end
229
234
  end
230
235
 
236
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
231
237
  def setup_audio_image_options(opts, options)
232
238
  opts.separator "\nAudio & Image Options:"
233
239
 
@@ -264,6 +270,7 @@ module AIA
264
270
  end
265
271
  end
266
272
 
273
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
267
274
  def setup_tool_options(opts, options)
268
275
  opts.separator "\nTool & Extension Options:"
269
276
 
@@ -303,6 +310,7 @@ module AIA
303
310
  setup_meta_options(opts, options)
304
311
  end
305
312
 
313
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
306
314
  def setup_logging_options(opts, options)
307
315
  opts.on("--log-level LEVEL", "Set log level (debug|info|warn|error|fatal)") do |level|
308
316
  level = level.downcase
@@ -337,6 +345,7 @@ module AIA
337
345
  end
338
346
  end
339
347
 
348
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
340
349
  def setup_output_options(opts, options)
341
350
  opts.on("--[no-]thinking", "Show raw thinking/reasoning blocks in output (default: off)") do |v|
342
351
  options[:thinking] = v
@@ -364,6 +373,7 @@ module AIA
364
373
  end
365
374
  end
366
375
 
376
+ # :reek:TooManyStatements -- one opts.on registration per CLI flag; splitting the group would scatter related flags
367
377
  def setup_mcp_options(opts, options)
368
378
  opts.on("--mcp FILE", "Load MCP server(s) from JSON file (repeatable)") do |file|
369
379
  options[:mcp_files] ||= []
@@ -440,11 +450,12 @@ module AIA
440
450
  #
441
451
  # @param model_string [String] comma-separated models with optional roles
442
452
  # @return [Array<Hash>] array of model specs
453
+ # :reek:TooManyStatements -- one-pass parser over MODEL[=ROLE] specs sharing the instance counter
443
454
  def parse_models_with_roles(model_string)
444
455
  models = []
445
456
  model_counts = Hash.new(0)
446
457
 
447
- # rubocop:disable Metrics/BlockLength
458
+ # rubocop:disable-next Metrics/BlockLength
448
459
  model_string.split(',').each do |spec|
449
460
  spec.strip!
450
461
 
@@ -480,46 +491,45 @@ module AIA
480
491
  }
481
492
  end
482
493
  end
483
- # rubocop:enable Metrics/BlockLength
484
494
 
485
495
  models
486
496
  end
487
497
 
488
498
  def validate_role_exists(role_id)
489
- if AIA::SkillUtils.path_based_id?(role_id)
490
- expanded = File.expand_path(role_id)
491
- expanded += '.md' if File.extname(expanded).empty?
492
- raise ArgumentError, "Role file not found: #{expanded}" unless File.exist?(expanded)
499
+ return validate_role_path!(role_id) if AIA::SkillUtils.path_based_id?(role_id)
493
500
 
494
- return
495
- end
496
-
497
- prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
501
+ prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
498
502
  roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
499
-
500
- unless role_id.start_with?(roles_prefix)
501
- role_id = "#{roles_prefix}/#{role_id}"
502
- end
503
+ role_id = "#{roles_prefix}/#{role_id}" unless role_id.start_with?(roles_prefix)
503
504
 
504
505
  role_file_path = File.join(prompts_dir, "#{role_id}.md")
505
-
506
506
  return if File.exist?(role_file_path)
507
- available_roles = list_available_role_names(prompts_dir, roles_prefix)
508
507
 
509
- error_msg = "Role file not found: #{role_file_path}\n\n"
508
+ raise ArgumentError, role_not_found_message(role_file_path, prompts_dir, roles_prefix)
509
+ end
510
+
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
517
+
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"
510
521
 
511
522
  if available_roles.empty?
512
- error_msg += "No roles directory found at #{File.join(prompts_dir, roles_prefix)}\n"
513
- error_msg += "Create the directory and add role files to use this feature."
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."
514
525
  else
515
- error_msg += "Available roles:\n"
516
- error_msg += available_roles.map { |r| " - #{r}" }.join("\n")
517
- error_msg += "\n\nCreate the role file or use an existing role."
526
+ message + "Available roles:\n" +
527
+ available_roles.map { |r| " - #{r}" }.join("\n") +
528
+ "\n\nCreate the role file or use an existing role."
518
529
  end
519
-
520
- raise ArgumentError, error_msg
521
530
  end
522
531
 
532
+ # :reek:TooManyStatements -- sequential terminal report: guards, then a markdown table per role; exits after printing
523
533
  def list_available_roles
524
534
  prompts_dir = ENV.fetch('AIA_PROMPTS__DIR', File.join(Dir.home, '.prompts'))
525
535
  roles_prefix = ENV.fetch('AIA_PROMPTS__ROLES_PREFIX', 'roles')
@@ -565,48 +575,19 @@ module AIA
565
575
  .sort
566
576
  end
567
577
 
568
- # rubocop:disable Metrics/AbcSize
578
+ # :reek:TooManyStatements -- sequential --available-models report: parse query, print matches, summary, exit
569
579
  def list_available_models(query)
570
580
  require 'ruby_llm'
571
581
 
572
- query = if query.nil?
573
- []
574
- else
575
- query.split(',')
576
- end
577
- # rubocop:enable Metrics/AbcSize
582
+ query = query.nil? ? [] : query.split(',')
583
+ modality_terms, substring_terms = parse_model_query(query)
578
584
 
579
585
  header = "\nAvailable LLMs"
580
586
  header += " for #{query.join(' and ')}" if query.any?
581
-
582
587
  puts header + ':'
583
588
  puts
584
589
 
585
- q1 = query.select { |q| q.include?('_to_') }.map { |q| q[0] == ':' ? q[1..] : q }
586
- q2 = query.reject { |q| q.include?('_to_') }
587
-
588
- counter = 0
589
-
590
- RubyLLM.models.all.each do |llm|
591
- inputs = llm.modalities.input.join(',')
592
- outputs = llm.modalities.output.join(',')
593
- entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}"
594
-
595
- if query.nil? || query.empty?
596
- counter += 1
597
- puts entry
598
- next
599
- end
600
-
601
- show_it = true
602
- q1.each { |q| show_it &&= llm.modalities.send("#{q}?") }
603
- q2.each { |q| show_it &&= entry.include?(q) }
604
-
605
- if show_it
606
- counter += 1
607
- puts entry
608
- end
609
- end
590
+ counter = print_matching_models(modality_terms, substring_terms)
610
591
 
611
592
  puts if counter.positive?
612
593
  puts "#{counter} LLMs matching your query"
@@ -614,8 +595,36 @@ module AIA
614
595
 
615
596
  exit
616
597
  end
598
+
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
606
+ end
607
+ end
608
+
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
615
+
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) }
624
+ end
617
625
  # rubocop:enable Metrics/ModuleLength
618
626
 
627
+ # :reek:DuplicateMethodCall -- each `exit 1` terminates a distinct validation failure; there is no value to hoist
619
628
  def process_tools_paths(path_list)
620
629
  paths = []
621
630
 
@@ -45,6 +45,7 @@ module AIA
45
45
  #
46
46
  # @param file_paths [Array<String>] paths to JSON configuration files
47
47
  # @return [Array<Hash>] array of server configurations with nested transport
48
+ # :reek:TooManyStatements -- per-file loop with warn-and-continue handling for missing files, bad JSON, and read errors
48
49
  def parse_files(file_paths)
49
50
  return [] if file_paths.nil? || file_paths.empty?
50
51
 
@@ -93,23 +94,29 @@ module AIA
93
94
  # @return [Array<Hash>] array of server configurations
94
95
  def convert_mcp_servers_format(mcp_servers)
95
96
  mcp_servers.map do |name, config|
96
- transport = { type: config['type'] || 'stdio' }
97
- transport[:command] = config['command'] if config['command']
98
- transport[:args] = Array(config['args']) if config['args']
99
- transport[:env] = config['env'] if config['env']
100
- transport[:url] = config['url'] if config['url']
101
- transport[:headers] = config['headers'] if config['headers']
102
-
103
- server = { name: name, transport: transport }
97
+ server = { name: name, transport: build_transport(config) }
104
98
  server[:timeout] = config['timeout'].to_i if config['timeout']
99
+ server.merge!(routing_metadata(config))
100
+ end
101
+ end
105
102
 
106
- # Preserve routing metadata for KBS/AIA
107
- server[:topics] = Array(config['topics']) if config['topics']
108
- server[:independent] = config['independent'] unless config['independent'].nil?
109
- server[:group] = config['group'] if config['group']
103
+ def build_transport(config)
104
+ transport = { type: config['type'] || 'stdio' }
105
+ transport[:command] = config['command'] if config['command']
106
+ transport[:args] = Array(config['args']) if config['args']
107
+ transport[:env] = config['env'] if config['env']
108
+ transport[:url] = config['url'] if config['url']
109
+ transport[:headers] = config['headers'] if config['headers']
110
+ transport
111
+ end
110
112
 
111
- server
112
- end
113
+ # Routing metadata preserved for KBS/AIA
114
+ def routing_metadata(config)
115
+ meta = {}
116
+ meta[:topics] = Array(config['topics']) if config['topics']
117
+ meta[:independent] = config['independent'] unless config['independent'].nil?
118
+ meta[:group] = config['group'] if config['group']
119
+ meta
113
120
  end
114
121
 
115
122
  # Convert simple format to robot_lab nested transport format
@@ -123,12 +130,13 @@ module AIA
123
130
 
124
131
  transport = { type: parsed['type'] || 'stdio' }
125
132
 
126
- if parsed['command'].is_a?(Array)
133
+ command = parsed['command']
134
+ if command.is_a?(Array)
127
135
  # Command is an array: first element is command, rest are args
128
- transport[:command] = parsed['command'].first
129
- transport[:args] = parsed['command'][1..] || []
130
- elsif parsed['command']
131
- transport[:command] = parsed['command']
136
+ transport[:command] = command.first
137
+ transport[:args] = command[1..] || []
138
+ elsif command
139
+ transport[:command] = command
132
140
  transport[:args] = parsed['args'] || []
133
141
  end
134
142