aidp 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 931d08a58cf52b7acc999d7e3320181c29710c50922463f4fb5c4e0a76c02e04
4
- data.tar.gz: 1971534889d0fde6daa6d8da6f345ffac4a698f827a205e445f75e7fa26d38da
3
+ metadata.gz: 00cbc8656c3ce1550f5246837062879c70a0206fa62c00a7bef6cd20979b9be5
4
+ data.tar.gz: 353616de5a90d4cf722673040d6e7fba09d60ff63618a3c135688202694e3075
5
5
  SHA512:
6
- metadata.gz: b87667e112faeb9818187eb97e281539e936ad213dcd49867781b37ff57ae9c827308cb9d4f7bdf707e48133fc3efcc79d1815d5a9b12cb3b395aad5d0e432cf
7
- data.tar.gz: 5b9fa041a4e680115248fad3fad2ccd7c8ab9cc956a471051b2e2945b1d2277cfea2af7f1d3f105dd36e8e9be1f149f42fb91080496f055deb0c26e8daef1427
6
+ metadata.gz: 65109acb0af57d4fefccbefa626f928cec5faaa3eedb5dfe4c0141521e03d95c8f7ca2dac8455baebdf6c14d0cbcd6cd81557a0bf4b2f0cdadb62547f60b414d
7
+ data.tar.gz: ea289ac3406776ccc1c602795fe6fa0a54c09fe8b3729b7e0f3b0151ba2782613cc2fee68b1111f9f6b81c78efeb7d31e77ab233ea49fba48e6d5585505e60f8
@@ -4,28 +4,28 @@ require "tty-table"
4
4
  require "tty-prompt"
5
5
  require "tty-spinner"
6
6
  require_relative "../harness/model_registry"
7
- require_relative "../harness/model_discovery_service"
7
+ require_relative "../harness/ruby_llm_registry"
8
8
 
9
9
  module Aidp
10
10
  class CLI
11
11
  # Command handler for `aidp models` subcommand group
12
12
  #
13
- # Provides commands for viewing and discovering AI models:
13
+ # Provides commands for viewing AI models from the RubyLLM registry:
14
14
  # - list: Show all available models with tier information
15
- # - discover: Discover models from configured providers
16
- # - refresh: Refresh the model cache
15
+ # - discover: Discover models from RubyLLM registry for a provider
16
+ # - validate: Validate model configuration
17
17
  #
18
18
  # Usage:
19
19
  # aidp models list [--provider=<name>] [--tier=<tier>]
20
20
  # aidp models discover [--provider=<name>]
21
- # aidp models refresh [--provider=<name>]
21
+ # aidp models validate
22
22
  class ModelsCommand
23
23
  include Aidp::MessageDisplay
24
24
 
25
- def initialize(prompt: TTY::Prompt.new, registry: nil, discovery_service: nil)
25
+ def initialize(prompt: TTY::Prompt.new, registry: nil, ruby_llm_registry: nil)
26
26
  @prompt = prompt
27
27
  @registry = registry
28
- @discovery_service = discovery_service
28
+ @ruby_llm_registry = ruby_llm_registry
29
29
  end
30
30
 
31
31
  # Main entry point for models subcommand
@@ -39,9 +39,6 @@ module Aidp
39
39
  when "discover"
40
40
  args.shift
41
41
  run_discover_command(args)
42
- when "refresh"
43
- args.shift
44
- run_refresh_command(args)
45
42
  when "validate"
46
43
  args.shift
47
44
  run_validate_command(args)
@@ -58,16 +55,15 @@ module Aidp
58
55
  @registry ||= Aidp::Harness::ModelRegistry.new
59
56
  end
60
57
 
61
- def discovery_service
62
- @discovery_service ||= Aidp::Harness::ModelDiscoveryService.new
58
+ def ruby_llm_registry
59
+ @ruby_llm_registry ||= Aidp::Harness::RubyLLMRegistry.new
63
60
  end
64
61
 
65
62
  def display_help
66
63
  display_message("\nUsage: aidp models <subcommand> [options]", type: :info)
67
64
  display_message("\nSubcommands:", type: :info)
68
65
  display_message(" list List all available models with tier information", type: :info)
69
- display_message(" discover Discover models from configured providers", type: :info)
70
- display_message(" refresh Refresh the model discovery cache", type: :info)
66
+ display_message(" discover Discover models from RubyLLM registry for a provider", type: :info)
71
67
  display_message(" validate Validate model configuration for all tiers", type: :info)
72
68
  display_message("\nOptions:", type: :info)
73
69
  display_message(" --provider=<name> Filter/target specific provider", type: :info)
@@ -76,9 +72,7 @@ module Aidp
76
72
  display_message(" aidp models list", type: :info)
77
73
  display_message(" aidp models list --tier=mini", type: :info)
78
74
  display_message(" aidp models list --provider=anthropic", type: :info)
79
- display_message(" aidp models discover", type: :info)
80
75
  display_message(" aidp models discover --provider=anthropic", type: :info)
81
- display_message(" aidp models refresh", type: :info)
82
76
  display_message(" aidp models validate", type: :info)
83
77
  end
84
78
 
@@ -86,42 +80,35 @@ module Aidp
86
80
  options = parse_list_options(args)
87
81
 
88
82
  begin
89
- # Get all model families from registry
90
- families = registry.all_families
91
-
92
- # Apply tier filter if specified
93
- if options[:tier]
94
- families = families.select { |family|
95
- info = registry.get_model_info(family)
96
- info && info["tier"] == options[:tier]
97
- }
83
+ # Get models from RubyLLM registry
84
+ all_model_ids = if options[:provider]
85
+ ruby_llm_registry.models_for_provider(options[:provider])
86
+ else
87
+ # Get all models by iterating known providers
88
+ known_providers = %w[anthropic openai google azure bedrock openrouter]
89
+ known_providers.flat_map { |p| ruby_llm_registry.models_for_provider(p) }.uniq
98
90
  end
99
91
 
100
92
  # Build table rows
101
93
  rows = []
102
- families.each do |family|
103
- info = registry.get_model_info(family)
94
+ all_model_ids.each do |model_id|
95
+ info = ruby_llm_registry.get_model_info(model_id)
104
96
  next unless info
105
97
 
106
- # Get providers that support this family
107
- providers = find_providers_for_family(family)
98
+ # Map advanced -> pro for display consistency
99
+ display_tier = (info[:tier] == "advanced") ? "pro" : info[:tier]
108
100
 
109
- # Apply provider filter if specified
110
- next if options[:provider] && !providers.include?(options[:provider])
111
-
112
- # Add a row for each provider that supports this family
113
- if providers.empty?
114
- # No provider support - show as registry-only
115
- rows << build_table_row(nil, family, info, "registry")
116
- else
117
- providers.each do |provider_name|
118
- rows << build_table_row(provider_name, family, info, "registry")
119
- end
101
+ # Apply tier filter if specified (handle pro/advanced mapping)
102
+ if options[:tier]
103
+ filter_tier = (options[:tier] == "pro") ? "advanced" : options[:tier]
104
+ next unless info[:tier] == filter_tier
120
105
  end
106
+
107
+ rows << build_table_row(info[:provider], model_id, info, display_tier)
121
108
  end
122
109
 
123
110
  # Sort rows by tier, then provider, then model name
124
- tier_order = {"mini" => 0, "standard" => 1, "advanced" => 2}
111
+ tier_order = {"mini" => 0, "standard" => 1, "pro" => 2, "advanced" => 2}
125
112
  rows.sort_by! { |r| [tier_order[r[2]] || 3, r[0] || "", r[1]] }
126
113
 
127
114
  # Display table
@@ -156,6 +143,7 @@ module Aidp
156
143
 
157
144
  def parse_list_options(args)
158
145
  options = {}
146
+ valid_tiers = %w[mini standard pro advanced]
159
147
 
160
148
  args.each do |arg|
161
149
  case arg
@@ -163,8 +151,8 @@ module Aidp
163
151
  options[:provider] = Regexp.last_match(1)
164
152
  when /^--tier=(.+)$/
165
153
  tier = Regexp.last_match(1)
166
- unless Aidp::Harness::ModelRegistry::VALID_TIERS.include?(tier)
167
- display_message("Invalid tier: #{tier}. Valid tiers: #{Aidp::Harness::ModelRegistry::VALID_TIERS.join(", ")}", type: :error)
154
+ unless valid_tiers.include?(tier)
155
+ display_message("Invalid tier: #{tier}. Valid tiers: #{valid_tiers.join(", ")}", type: :error)
168
156
  exit 1
169
157
  end
170
158
  options[:tier] = tier
@@ -177,18 +165,17 @@ module Aidp
177
165
  options
178
166
  end
179
167
 
180
- def build_table_row(provider_name, family, info, source)
181
- capabilities = (info["capabilities"] || []).join(",")
182
- context = format_context_window(info["context_window"])
183
- speed = info["speed"] || "unknown"
168
+ def build_table_row(provider_name, model_id, info, display_tier)
169
+ capabilities = (info[:capabilities] || []).join(",")
170
+ context = format_context_window(info[:context_window])
184
171
 
185
172
  [
186
173
  provider_name || "-",
187
- family,
188
- info["tier"] || "unknown",
174
+ model_id,
175
+ display_tier || "unknown",
189
176
  capabilities.empty? ? "-" : capabilities,
190
177
  context,
191
- speed
178
+ "-" # Speed not available in registry
192
179
  ]
193
180
  end
194
181
 
@@ -213,8 +200,8 @@ module Aidp
213
200
 
214
201
  def build_footer(count)
215
202
  tips = [
216
- "💡 Showing #{count} model#{"s" unless count == 1} from the static registry",
217
- "💡 Model families are provider-agnostic (e.g., 'claude-3-5-sonnet' works across providers)"
203
+ "💡 Showing #{count} model#{"s" unless count == 1} from RubyLLM registry",
204
+ "💡 Registry updated regularly via ruby_llm gem updates"
218
205
  ]
219
206
  tips.join("\n")
220
207
  end
@@ -249,81 +236,56 @@ module Aidp
249
236
  def run_discover_command(args)
250
237
  options = parse_discover_options(args)
251
238
 
252
- begin
253
- display_message("\n🔍 Discovering models from configured providers...\n", type: :highlight)
254
-
255
- spinner = TTY::Spinner.new("[:spinner] Querying provider APIs...", format: :dots)
256
- spinner.auto_spin
257
-
258
- # Discover models
259
- results = if options[:provider]
260
- {options[:provider] => discovery_service.discover_models(options[:provider], use_cache: false)}
261
- else
262
- discovery_service.discover_all_models(use_cache: false)
263
- end
239
+ unless options[:provider]
240
+ display_message("\n⚠️ Please specify a provider with --provider=<name>", type: :warning)
241
+ display_message("Example: aidp models discover --provider=anthropic\n", type: :info)
242
+ return 1
243
+ end
264
244
 
265
- spinner.success("✓")
245
+ begin
246
+ display_message("\n🔍 Discovering models for #{options[:provider]} from RubyLLM registry...\n", type: :highlight)
266
247
 
267
- # Display results
268
- total_models = 0
269
- results.each do |provider, models|
270
- next if models.empty?
271
-
272
- total_models += models.size
273
- display_message("\n✓ Found #{models.size} models for #{provider}:", type: :success)
274
-
275
- # Group by tier
276
- by_tier = models.group_by { |m| m[:tier] }
277
- %w[mini standard advanced].each do |tier|
278
- tier_models = by_tier[tier] || []
279
- next if tier_models.empty?
280
-
281
- display_message(" #{tier.capitalize} tier:", type: :info)
282
- tier_models.each do |model|
283
- display_message(" - #{model[:name]}", type: :info)
284
- end
285
- end
286
- end
248
+ # Get models from registry
249
+ model_ids = ruby_llm_registry.models_for_provider(options[:provider])
287
250
 
288
- if total_models == 0
289
- display_message("\n⚠️ No models discovered. Ensure provider CLIs are installed and configured.", type: :warning)
251
+ if model_ids.empty?
252
+ display_message("\n⚠️ No models found for provider '#{options[:provider]}'.", type: :warning)
253
+ display_message("Provider may not be supported or name may be incorrect.\n", type: :info)
290
254
  return 1
291
255
  end
292
256
 
293
- display_message("\n✅ Discovered #{total_models} total model#{"s" unless total_models == 1}", type: :success)
294
- display_message("💾 Models cached for 24 hours\n", type: :info)
295
- 0
296
- rescue => e
297
- display_message("Error discovering models: #{e.message}", type: :error)
298
- Aidp.log_error("models_command", "discovery error", error: e.message, backtrace: e.backtrace.first(5))
299
- 1
300
- end
301
- end
257
+ # Group by tier
258
+ models_by_tier = Hash.new { |h, k| h[k] = [] }
259
+ model_ids.each do |model_id|
260
+ info = ruby_llm_registry.get_model_info(model_id)
261
+ next unless info
302
262
 
303
- def run_refresh_command(args)
304
- options = parse_refresh_options(args)
263
+ # Map advanced -> pro for display
264
+ tier = (info[:tier] == "advanced") ? "pro" : info[:tier]
265
+ models_by_tier[tier] << model_id
266
+ end
305
267
 
306
- begin
307
- display_message("\n♻️ Refreshing model cache...\n", type: :highlight)
268
+ # Display results
269
+ display_message("\n Found #{model_ids.size} models for #{options[:provider]}:", type: :success)
308
270
 
309
- spinner = TTY::Spinner.new("[:spinner] Clearing cache and re-discovering...", format: :dots)
310
- spinner.auto_spin
271
+ %w[mini standard pro].each do |tier|
272
+ tier_models = models_by_tier[tier]
273
+ next if tier_models.empty?
311
274
 
312
- if options[:provider]
313
- discovery_service.refresh_cache(options[:provider])
314
- spinner.success("")
315
- display_message("\n✅ Refreshed cache for #{options[:provider]}", type: :success)
316
- else
317
- discovery_service.refresh_all_caches
318
- spinner.success("✓")
319
- display_message("\n✅ Refreshed cache for all providers", type: :success)
275
+ display_message("\n #{tier.capitalize} tier (#{tier_models.size} model#{"s" unless tier_models.size == 1}):", type: :info)
276
+ tier_models.first(5).each do |model|
277
+ display_message(" - #{model}", type: :info)
278
+ end
279
+ if tier_models.size > 5
280
+ display_message(" ... and #{tier_models.size - 5} more", type: :info)
281
+ end
320
282
  end
321
283
 
322
- display_message("💡 Run 'aidp models discover' to see the updated models\n", type: :info)
284
+ display_message("\n💡 Use 'aidp models list --provider=#{options[:provider]}' to see full details\n", type: :info)
323
285
  0
324
286
  rescue => e
325
- display_message("Error refreshing cache: #{e.message}", type: :error)
326
- Aidp.log_error("models_command", "refresh error", error: e.message, backtrace: e.backtrace.first(5))
287
+ display_message("Error discovering models: #{e.message}", type: :error)
288
+ Aidp.log_error("models_command", "discovery error", error: e.message, backtrace: e.backtrace.first(5))
327
289
  1
328
290
  end
329
291
  end
@@ -344,10 +306,6 @@ module Aidp
344
306
  options
345
307
  end
346
308
 
347
- def parse_refresh_options(args)
348
- parse_discover_options(args)
349
- end
350
-
351
309
  def run_validate_command(args)
352
310
  parse_validate_options(args)
353
311
 
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+ require "tty-table"
5
+ require_relative "../config"
6
+ require_relative "../metadata/cache"
7
+ require_relative "../metadata/query"
8
+ require_relative "../metadata/validator"
9
+
10
+ module Aidp
11
+ class CLI
12
+ # CLI commands for managing tool metadata
13
+ #
14
+ # Provides commands for:
15
+ # - aidp tools lint - Validate all metadata
16
+ # - aidp tools info <id> - Display tool details
17
+ # - aidp tools reload - Force cache regeneration
18
+ # - aidp tools list - List all tools
19
+ class ToolsCommand
20
+ # Initialize tools command
21
+ #
22
+ # @param project_dir [String] Project directory path
23
+ # @param prompt [TTY::Prompt] TTY prompt instance
24
+ def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new)
25
+ @project_dir = project_dir
26
+ @prompt = prompt
27
+ end
28
+
29
+ # Run tools command
30
+ #
31
+ # @param args [Array<String>] Command arguments
32
+ # @return [Integer] Exit code
33
+ def run(args)
34
+ subcommand = args.shift
35
+
36
+ case subcommand
37
+ when "lint"
38
+ run_lint
39
+ when "info"
40
+ tool_id = args.shift
41
+ unless tool_id
42
+ @prompt.say("Error: tool ID required")
43
+ @prompt.say("Usage: aidp tools info <tool_id>")
44
+ return 1
45
+ end
46
+ run_info(tool_id)
47
+ when "reload"
48
+ run_reload
49
+ when "list"
50
+ run_list
51
+ when nil, "help", "--help", "-h"
52
+ show_help
53
+ 0
54
+ else
55
+ @prompt.say("Unknown subcommand: #{subcommand}")
56
+ show_help
57
+ 1
58
+ end
59
+ end
60
+
61
+ # Show help message
62
+ def show_help
63
+ @prompt.say("\nAIDP Tools Management")
64
+ @prompt.say("\nUsage:")
65
+ @prompt.say(" aidp tools lint Validate all tool metadata")
66
+ @prompt.say(" aidp tools info <id> Show detailed tool information")
67
+ @prompt.say(" aidp tools reload Force regenerate tool directory cache")
68
+ @prompt.say(" aidp tools list List all available tools")
69
+ @prompt.say("\nExamples:")
70
+ @prompt.say(" aidp tools lint")
71
+ @prompt.say(" aidp tools info ruby_rspec_tdd")
72
+ @prompt.say(" aidp tools reload")
73
+ end
74
+
75
+ # Run lint command
76
+ def run_lint
77
+ Aidp.log_info("tools", "Running tool metadata lint")
78
+
79
+ @prompt.say("\nValidating tool metadata...")
80
+
81
+ cache = create_cache
82
+ query = Metadata::Query.new(cache: cache)
83
+
84
+ begin
85
+ query.directory
86
+ rescue => e
87
+ @prompt.error("Failed to load tool directory: #{e.message}")
88
+ Aidp.log_error("tools", "Lint failed", error: e.message)
89
+ return 1
90
+ end
91
+
92
+ # Reload to get fresh validation
93
+ tools = load_all_tools
94
+
95
+ validator = Metadata::Validator.new(tools)
96
+ results = validator.validate_all
97
+
98
+ # Display results
99
+ display_lint_results(results)
100
+
101
+ # Write error log if there are errors
102
+ invalid_results = results.reject(&:valid)
103
+ if invalid_results.any?
104
+ log_path = File.join(@project_dir, ".aidp", "logs", "metadata_errors.log")
105
+ validator.write_error_log(results, log_path)
106
+ @prompt.warn("\nError log written to: #{log_path}")
107
+ end
108
+
109
+ invalid_results.empty? ? 0 : 1
110
+ end
111
+
112
+ # Run info command
113
+ #
114
+ # @param tool_id [String] Tool ID
115
+ def run_info(tool_id)
116
+ Aidp.log_info("tools", "Showing tool info", tool_id: tool_id)
117
+
118
+ cache = create_cache
119
+ query = Metadata::Query.new(cache: cache)
120
+
121
+ tool = query.find_by_id(tool_id)
122
+
123
+ unless tool
124
+ @prompt.error("Tool not found: #{tool_id}")
125
+ return 1
126
+ end
127
+
128
+ display_tool_details(tool)
129
+
130
+ 0
131
+ end
132
+
133
+ # Run reload command
134
+ def run_reload
135
+ Aidp.log_info("tools", "Reloading tool directory")
136
+
137
+ @prompt.say("\nRegenerating tool directory cache...")
138
+
139
+ cache = create_cache
140
+
141
+ begin
142
+ cache.reload
143
+ @prompt.ok("Tool directory cache regenerated successfully")
144
+ rescue => e
145
+ @prompt.error("Failed to reload cache: #{e.message}")
146
+ Aidp.log_error("tools", "Reload failed", error: e.message)
147
+ return 1
148
+ end
149
+
150
+ 0
151
+ end
152
+
153
+ # Run list command
154
+ def run_list
155
+ Aidp.log_info("tools", "Listing all tools")
156
+
157
+ cache = create_cache
158
+ query = Metadata::Query.new(cache: cache)
159
+
160
+ query.directory
161
+ stats = query.statistics
162
+
163
+ @prompt.say("\nTool Directory Statistics:")
164
+ @prompt.say(" Total tools: #{stats["total_tools"]}")
165
+ @prompt.say(" By type:")
166
+ stats["by_type"].each do |type, count|
167
+ @prompt.say(" #{type}: #{count}")
168
+ end
169
+
170
+ @prompt.say("\nAll Tools:")
171
+
172
+ # Group by type
173
+ %w[skill persona template].each do |type|
174
+ tools = query.find_by_type(type)
175
+ next if tools.empty?
176
+
177
+ @prompt.say("\n#{type.capitalize}s (#{tools.size}):")
178
+ display_tools_table(tools)
179
+ end
180
+
181
+ 0
182
+ end
183
+
184
+ private
185
+
186
+ # Create metadata cache instance
187
+ #
188
+ # @return [Metadata::Cache] Cache instance
189
+ def create_cache
190
+ tool_config = Aidp::Config.tool_metadata_config(@project_dir)
191
+
192
+ directories = tool_config[:directories] || default_directories
193
+ cache_file = tool_config[:cache_file] || default_cache_file
194
+ strict = tool_config[:strict] || false
195
+
196
+ Metadata::Cache.new(
197
+ cache_path: cache_file,
198
+ directories: directories,
199
+ strict: strict
200
+ )
201
+ end
202
+
203
+ # Get default directories to scan
204
+ #
205
+ # @return [Array<String>] Default directories
206
+ def default_directories
207
+ [
208
+ File.join(@project_dir, ".aidp", "skills"),
209
+ File.join(@project_dir, ".aidp", "personas"),
210
+ File.join(@project_dir, ".aidp", "templates"),
211
+ # Also include gem templates
212
+ File.expand_path("../../templates/skills", __dir__),
213
+ File.expand_path("../../templates", __dir__)
214
+ ].select { |dir| Dir.exist?(dir) }
215
+ end
216
+
217
+ # Get default cache file path
218
+ #
219
+ # @return [String] Cache file path
220
+ def default_cache_file
221
+ File.join(@project_dir, ".aidp", "cache", "tool_directory.json")
222
+ end
223
+
224
+ # Load all tools from directories
225
+ #
226
+ # @return [Array<Metadata::ToolMetadata>] All tools
227
+ def load_all_tools
228
+ directories = default_directories
229
+ scanner = Metadata::Scanner.new(directories)
230
+ scanner.scan_all
231
+ end
232
+
233
+ # Display lint results
234
+ #
235
+ # @param results [Array<Metadata::Validator::ValidationResult>] Results
236
+ def display_lint_results(results)
237
+ valid_count = results.count(&:valid)
238
+ invalid_count = results.count { |r| !r.valid }
239
+ warning_count = results.sum { |r| r.warnings.size }
240
+
241
+ @prompt.say("\nValidation Results:")
242
+ @prompt.say(" Total tools: #{results.size}")
243
+ @prompt.ok(" Valid: #{valid_count}") if valid_count > 0
244
+ @prompt.error(" Invalid: #{invalid_count}") if invalid_count > 0
245
+ @prompt.warn(" Warnings: #{warning_count}") if warning_count > 0
246
+
247
+ # Show errors
248
+ invalid_results = results.reject(&:valid)
249
+ if invalid_results.any?
250
+ @prompt.say("\nErrors:")
251
+ invalid_results.each do |result|
252
+ @prompt.error("\n #{result.tool_id} (#{result.file_path}):")
253
+ result.errors.each { |err| @prompt.say(" - #{err}") }
254
+ end
255
+ end
256
+
257
+ # Show warnings
258
+ warnings = results.select { |r| r.warnings.any? }
259
+ if warnings.any?
260
+ @prompt.say("\nWarnings:")
261
+ warnings.each do |result|
262
+ @prompt.warn("\n #{result.tool_id} (#{result.file_path}):")
263
+ result.warnings.each { |warn| @prompt.say(" - #{warn}") }
264
+ end
265
+ end
266
+
267
+ if invalid_count.zero? && warning_count.zero?
268
+ @prompt.ok("\nAll tools validated successfully!")
269
+ end
270
+ end
271
+
272
+ # Display tool details
273
+ #
274
+ # @param tool [Hash] Tool metadata
275
+ def display_tool_details(tool)
276
+ @prompt.say("\nTool: #{tool["title"]}")
277
+ @prompt.say("=" * 60)
278
+ @prompt.say("ID: #{tool["id"]}")
279
+ @prompt.say("Type: #{tool["type"]}")
280
+ @prompt.say("Version: #{tool["version"]}")
281
+ @prompt.say("Summary: #{tool["summary"]}")
282
+ @prompt.say("Priority: #{tool["priority"]}")
283
+ @prompt.say("Experimental: #{tool["experimental"]}")
284
+
285
+ if tool["applies_to"]&.any?
286
+ @prompt.say("\nApplies To:")
287
+ tool["applies_to"].each { |tag| @prompt.say(" - #{tag}") }
288
+ end
289
+
290
+ if tool["work_unit_types"]&.any?
291
+ @prompt.say("\nWork Unit Types:")
292
+ tool["work_unit_types"].each { |wut| @prompt.say(" - #{wut}") }
293
+ end
294
+
295
+ if tool["capabilities"]&.any?
296
+ @prompt.say("\nCapabilities:")
297
+ tool["capabilities"].each { |cap| @prompt.say(" - #{cap}") }
298
+ end
299
+
300
+ if tool["dependencies"]&.any?
301
+ @prompt.say("\nDependencies:")
302
+ tool["dependencies"].each { |dep| @prompt.say(" - #{dep}") }
303
+ end
304
+
305
+ @prompt.say("\nSource: #{tool["source_path"]}")
306
+ end
307
+
308
+ # Display tools in a table
309
+ #
310
+ # @param tools [Array<Hash>] Tools to display
311
+ def display_tools_table(tools)
312
+ return if tools.empty?
313
+
314
+ rows = tools.map do |tool|
315
+ [
316
+ tool["id"],
317
+ tool["title"],
318
+ tool["version"],
319
+ tool["priority"],
320
+ (tool["applies_to"] || []).join(", ")
321
+ ]
322
+ end
323
+
324
+ table = TTY::Table.new(
325
+ header: ["ID", "Title", "Version", "Priority", "Tags"],
326
+ rows: rows
327
+ )
328
+
329
+ @prompt.say(table.render(:unicode))
330
+ end
331
+ end
332
+ end
333
+ end