legionio 1.4.66 → 1.4.68

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: ce930ae41174649616f7c9793f36ba8d9c3b3d4655f778b34a10648715cdef35
4
- data.tar.gz: 2cddda47aa6fb16bbaa6245daecb433ca572c60c2c689bfdb2aa7af6feb45349
3
+ metadata.gz: 1521235e6e865bd6d74f41b93c57e159de23d9dee0f044486c8059a6af780ab0
4
+ data.tar.gz: 7bdb02eb37ca2f03b9c0b6f7f494ebbb394d69f53ff83dd4bb5c1e27cb37e85e
5
5
  SHA512:
6
- metadata.gz: 9c9481c8ec290af1037a6457268742bf05f1c8034073c57728726d426a8d995e5bb1a2619d1e1906cf69a1e7629019461a75d4cb4f1718243f5a2d4f5386f777
7
- data.tar.gz: 6607b7b494cfeb13a33e1101584c679107a7616d58004ef41d81984056b20a88fdb3ad0377fa43734d9e411d27bc22be6b6629429730c52181fbfe370c261192
6
+ metadata.gz: 78a7171285d6a6eebda11d87d2782a68b6edb0dbc1f0be6d6034c1b4c7e535a5ae2dc1feb34ecbcc5af64d767759c7af7c78c1c50ed8672a1f08dea738c6b23b
7
+ data.tar.gz: 0b7c0e4d1298e13815383df8667d2b14debf66675c981002847fa19a01d0be6afa4314d811ba7cde7285ee46ddf37e9985e8a86745aef2b4b122d2a7dc1e16ac
data/.rubocop.yml CHANGED
@@ -40,6 +40,7 @@ Metrics/BlockLength:
40
40
  - 'lib/legion/api/auth_worker.rb'
41
41
  - 'lib/legion/api/auth_human.rb'
42
42
  - 'lib/legion/cli/auth_command.rb'
43
+ - 'lib/legion/cli/detect_command.rb'
43
44
 
44
45
  Metrics/AbcSize:
45
46
  Max: 60
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.68] - 2026-03-19
4
+
5
+ ### Added
6
+ - `legionio llm` subcommand for LLM provider diagnostics
7
+ - `llm status` (default) — show LLM state, enabled providers, routing, system memory
8
+ - `llm providers` — list all providers with enabled/disabled and reachability status
9
+ - `llm models` — list available models per enabled provider (Ollama discovery + cloud defaults)
10
+ - `llm ping` — test connectivity to each enabled provider with latency measurement
11
+ - All subcommands support `--json` output
12
+ - `legionio version` now shows legion-llm, legion-gaia, and legion-tty in components list
13
+ - `legionio version --json` now includes components hash and extension count
14
+
15
+ ### Fixed
16
+ - `legionio update` now correctly detects gem version changes (was showing "already latest" for every gem due to stale in-memory gem spec cache after subprocess install)
17
+
18
+ ## [1.4.67] - 2026-03-18
19
+
20
+ ### Added
21
+ - `legionio detect` subcommand — scan environment and recommend extensions (requires lex-detect gem)
22
+ - `detect scan` (default) — show detected software and recommended extensions
23
+ - `detect catalog` — show full detection catalog
24
+ - `detect missing` — list extensions that should be installed
25
+ - `--install` flag to install missing extensions after scan
26
+ - `--json` output mode
27
+ - `legionio update` now suggests new extensions via lex-detect after updating gems
28
+
3
29
  ## [1.4.66] - 2026-03-18
4
30
 
5
31
  ### Fixed
data/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/LegionIO
11
11
  **Gem**: `legionio`
12
- **Version**: 1.4.65
12
+ **Version**: 1.4.67
13
13
  **License**: Apache-2.0
14
14
  **Docker**: `legionio/legion`
15
15
  **Ruby**: >= 3.4
@@ -501,9 +501,11 @@ legion
501
501
  | `bootsnap` (>= 1.18) | YARV bytecode + load-path caching |
502
502
  | `oj` (>= 3.16) | Fast JSON (C extension) |
503
503
  | `puma` (>= 6.0) | HTTP server for API |
504
+ | `rackup` (>= 2.0) | Rack server launcher for MCP HTTP transport |
504
505
  | `mcp` (~> 0.8) | MCP server SDK |
505
506
  | `reline` (>= 0.5) | Interactive line editing for chat REPL |
506
507
  | `rouge` (>= 4.0) | Syntax highlighting for chat markdown rendering |
508
+ | `tty-spinner` (~> 0.9) | Spinner animation for CLI loading states |
507
509
  | `sinatra` (>= 4.0) | HTTP API framework |
508
510
  | `thor` (>= 1.3) | CLI framework |
509
511
 
data/README.md CHANGED
@@ -14,7 +14,7 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
14
14
  ╰──────────────────────────────────────╯
15
15
  ```
16
16
 
17
- **Ruby >= 3.4** | **v1.4.61** | **Apache-2.0** | [@Esity](https://github.com/Esity)
17
+ **Ruby >= 3.4** | **v1.4.67** | **Apache-2.0** | [@Esity](https://github.com/Esity)
18
18
 
19
19
  ---
20
20
 
@@ -83,6 +83,7 @@ gem 'legionio'
83
83
  | `legion-llm` | AI chat, commit, review, agents, multi-provider LLM routing |
84
84
  | `legion-cache` | Redis/Memcached caching for extensions |
85
85
  | `legion-crypt` | Vault integration, encryption, JWT auth |
86
+ | `legion-tty` | TTY UI components (spinners, tables, prompts) |
86
87
 
87
88
  ## Infrastructure
88
89
 
@@ -487,7 +488,7 @@ Each phase registers with `Legion::Readiness`. All phases are individually toggl
487
488
  git clone https://github.com/LegionIO/LegionIO.git
488
489
  cd LegionIO
489
490
  bundle install
490
- bundle exec rspec # 1379 examples, 0 failures
491
+ bundle exec rspec # 0 failures
491
492
  bundle exec rubocop # 0 offenses
492
493
  ```
493
494
 
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'legion/cli/output'
5
+
6
+ module Legion
7
+ module CLI
8
+ class Detect < Thor
9
+ namespace 'detect'
10
+
11
+ def self.exit_on_failure?
12
+ true
13
+ end
14
+
15
+ class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
16
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
17
+
18
+ default_task :scan
19
+
20
+ desc 'scan', 'Scan environment and recommend extensions (default)'
21
+ option :install, type: :boolean, default: false, desc: 'Install missing extensions after scan'
22
+ option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
23
+ def scan
24
+ out = formatter
25
+ require_detect_gem
26
+
27
+ results = Legion::Extensions::Detect.scan
28
+
29
+ if options[:json]
30
+ out.json(detections: results)
31
+ else
32
+ display_detections(out, results)
33
+ install_missing(out) if options[:install]
34
+ end
35
+ end
36
+
37
+ desc 'catalog', 'Show the full detection catalog'
38
+ def catalog
39
+ out = formatter
40
+ require_detect_gem
41
+
42
+ catalog = Legion::Extensions::Detect.catalog
43
+
44
+ if options[:json]
45
+ catalog_data = catalog.map do |rule|
46
+ { name: rule[:name], extensions: rule[:extensions],
47
+ signals: rule[:signals].map { |s| "#{s[:type]}:#{s[:match]}" } }
48
+ end
49
+ out.json(catalog: catalog_data)
50
+ else
51
+ out.header('Detection Catalog')
52
+ out.spacer
53
+ catalog.each do |rule|
54
+ signals = rule[:signals].map { |s| "#{s[:type]}:#{s[:match]}" }.join(', ')
55
+ extensions = rule[:extensions].join(', ')
56
+ puts " #{out.colorize(rule[:name].ljust(20), :label)} #{extensions.ljust(30)} #{signals}"
57
+ end
58
+ out.spacer
59
+ puts " #{catalog.size} detection rules"
60
+ end
61
+ end
62
+
63
+ desc 'missing', 'List extensions that should be installed but are not'
64
+ def missing
65
+ out = formatter
66
+ require_detect_gem
67
+
68
+ missing_gems = Legion::Extensions::Detect.missing
69
+
70
+ if options[:json]
71
+ out.json(missing: missing_gems)
72
+ elsif missing_gems.empty?
73
+ out.success('All detected extensions are installed')
74
+ else
75
+ out.header('Missing Extensions')
76
+ missing_gems.each { |name| puts " gem install #{name}" }
77
+ out.spacer
78
+ puts " #{missing_gems.size} extension(s) recommended"
79
+ puts " Run 'legionio detect --install' to install them"
80
+ end
81
+ end
82
+
83
+ no_commands do
84
+ def formatter
85
+ @formatter ||= Output::Formatter.new(
86
+ json: options[:json],
87
+ color: !options[:no_color]
88
+ )
89
+ end
90
+
91
+ private
92
+
93
+ def require_detect_gem
94
+ require 'legion/extensions/detect'
95
+ rescue LoadError => e
96
+ formatter.error("lex-detect gem not installed: #{e.message}")
97
+ puts ' Install with: gem install lex-detect'
98
+ raise SystemExit, 1
99
+ end
100
+
101
+ def display_detections(out, results)
102
+ if results.empty?
103
+ out.detail('No software detected that maps to Legion extensions.')
104
+ return
105
+ end
106
+
107
+ out.header('Environment Detection')
108
+ out.spacer
109
+
110
+ installed_count = 0
111
+ total_count = 0
112
+
113
+ results.each do |detection|
114
+ signals = detection[:matched_signals].join(', ')
115
+ detection[:extensions].each do |ext|
116
+ total_count += 1
117
+ is_installed = detection[:installed][ext]
118
+ installed_count += 1 if is_installed
119
+ status = is_installed ? out.colorize('installed', :success) : out.colorize('missing', :error)
120
+ puts " #{out.colorize(detection[:name].ljust(20), :label)} #{signals.ljust(35)} #{ext.ljust(25)} #{status}"
121
+ end
122
+ end
123
+
124
+ out.spacer
125
+ puts " #{installed_count} of #{total_count} extension(s) installed"
126
+ end
127
+
128
+ def install_missing(out)
129
+ missing_gems = Legion::Extensions::Detect.missing
130
+ return if missing_gems.empty?
131
+
132
+ out.spacer
133
+ if options[:dry_run]
134
+ out.header('Would install')
135
+ missing_gems.each { |name| puts " #{name}" }
136
+ return
137
+ end
138
+
139
+ out.header('Installing missing extensions')
140
+ result = Legion::Extensions::Detect.install_missing!
141
+
142
+ result[:installed].each { |name| out.success(" Installed #{name}") }
143
+ result[:failed].each { |f| out.error(" Failed: #{f[:name]} — #{f[:error]}") }
144
+
145
+ out.spacer
146
+ if result[:failed].empty?
147
+ out.success("#{result[:installed].size} extension(s) installed")
148
+ else
149
+ out.warn("#{result[:installed].size} installed, #{result[:failed].size} failed")
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ module Legion
6
+ module CLI
7
+ class Llm < Thor
8
+ namespace 'llm'
9
+
10
+ def self.exit_on_failure?
11
+ true
12
+ end
13
+
14
+ class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
15
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
16
+ class_option :verbose, type: :boolean, default: false, aliases: ['-V'], desc: 'Verbose logging'
17
+ class_option :config_dir, type: :string, desc: 'Config directory path'
18
+
19
+ desc 'status', 'Show LLM subsystem status and provider health'
20
+ default_task :status
21
+ def status
22
+ out = formatter
23
+ boot_llm_settings
24
+
25
+ data = collect_status
26
+ if options[:json]
27
+ out.json(data)
28
+ else
29
+ show_status(out, data)
30
+ end
31
+ end
32
+
33
+ desc 'providers', 'List configured LLM providers'
34
+ def providers
35
+ out = formatter
36
+ boot_llm_settings
37
+
38
+ data = collect_providers
39
+ if options[:json]
40
+ out.json(providers: data)
41
+ else
42
+ show_providers(out, data)
43
+ end
44
+ end
45
+
46
+ desc 'models', 'List available models per provider'
47
+ def models
48
+ out = formatter
49
+ boot_llm_settings
50
+
51
+ data = collect_models
52
+ if options[:json]
53
+ out.json(models: data)
54
+ else
55
+ show_models(out, data)
56
+ end
57
+ end
58
+
59
+ desc 'ping', 'Test connectivity to each enabled provider'
60
+ option :timeout, type: :numeric, default: 15, desc: 'Timeout per provider in seconds'
61
+ def ping
62
+ out = formatter
63
+ boot_llm(out)
64
+
65
+ results = ping_all_providers(out)
66
+ if options[:json]
67
+ out.json(results: results)
68
+ else
69
+ show_ping_results(out, results)
70
+ end
71
+ end
72
+
73
+ no_commands do # rubocop:disable Metrics/BlockLength
74
+ def formatter
75
+ @formatter ||= Output::Formatter.new(
76
+ json: options[:json],
77
+ color: !options[:no_color]
78
+ )
79
+ end
80
+
81
+ private
82
+
83
+ def boot_llm_settings
84
+ Connection.config_dir = options[:config_dir] if options[:config_dir]
85
+ Connection.log_level = options[:verbose] ? 'debug' : 'error'
86
+ Connection.ensure_settings
87
+ require 'legion/llm'
88
+ Legion::Settings.merge_settings(:llm, Legion::LLM::Settings.default)
89
+ end
90
+
91
+ def boot_llm(out)
92
+ boot_llm_settings
93
+ out.header('Starting LLM subsystem...') unless options[:json]
94
+ Legion::LLM.start
95
+ rescue StandardError => e
96
+ out.error("LLM start failed: #{e.message}") unless options[:json]
97
+ end
98
+
99
+ def llm_settings
100
+ Legion::LLM.settings
101
+ end
102
+
103
+ def collect_status
104
+ providers_cfg = llm_settings[:providers] || {}
105
+ enabled = providers_cfg.select { |_, c| c[:enabled] }
106
+ started = defined?(Legion::LLM) && Legion::LLM.started?
107
+
108
+ {
109
+ started: started,
110
+ default_model: llm_settings[:default_model],
111
+ default_provider: llm_settings[:default_provider],
112
+ enabled_count: enabled.size,
113
+ total_count: providers_cfg.size,
114
+ providers: collect_providers,
115
+ routing: collect_routing,
116
+ system: collect_system
117
+ }
118
+ end
119
+
120
+ def collect_providers
121
+ providers_cfg = llm_settings[:providers] || {}
122
+ providers_cfg.map do |name, cfg|
123
+ {
124
+ name: name,
125
+ enabled: cfg[:enabled] == true,
126
+ default_model: cfg[:default_model],
127
+ reachable: check_reachable(name, cfg)
128
+ }
129
+ end
130
+ end
131
+
132
+ def check_reachable(name, cfg)
133
+ case name
134
+ when :ollama
135
+ return false unless cfg[:enabled]
136
+
137
+ base = cfg[:base_url] || 'http://localhost:11434'
138
+ uri = URI(base)
139
+ Socket.tcp(uri.host, uri.port, connect_timeout: 2) { true }
140
+ when :bedrock
141
+ return nil unless cfg[:enabled]
142
+
143
+ cfg[:bearer_token] || (cfg[:api_key] && cfg[:secret_key]) ? :credentials_present : false
144
+ else
145
+ return nil unless cfg[:enabled]
146
+
147
+ cfg[:api_key] ? :credentials_present : false
148
+ end
149
+ rescue StandardError
150
+ false
151
+ end
152
+
153
+ def collect_routing
154
+ return { enabled: false } unless defined?(Legion::LLM::Router)
155
+
156
+ {
157
+ enabled: Legion::LLM::Router.routing_enabled?,
158
+ local_tier: Legion::LLM::Router.tier_available?(:local),
159
+ fleet_tier: Legion::LLM::Router.tier_available?(:fleet),
160
+ cloud_tier: Legion::LLM::Router.tier_available?(:cloud)
161
+ }
162
+ rescue StandardError
163
+ { enabled: false }
164
+ end
165
+
166
+ def collect_system
167
+ return {} unless defined?(Legion::LLM::Discovery::System)
168
+
169
+ Legion::LLM::Discovery::System.refresh! if Legion::LLM::Discovery::System.stale?
170
+ {
171
+ platform: Legion::LLM::Discovery::System.platform,
172
+ total_memory_mb: Legion::LLM::Discovery::System.total_memory_mb,
173
+ avail_memory_mb: Legion::LLM::Discovery::System.available_memory_mb,
174
+ memory_pressure: Legion::LLM::Discovery::System.memory_pressure?
175
+ }
176
+ rescue StandardError
177
+ {}
178
+ end
179
+
180
+ def collect_models
181
+ providers_cfg = llm_settings[:providers] || {}
182
+ result = {}
183
+
184
+ providers_cfg.each do |name, cfg|
185
+ next unless cfg[:enabled]
186
+
187
+ models = [cfg[:default_model]].compact
188
+ if name == :ollama && defined?(Legion::LLM::Discovery::Ollama)
189
+ begin
190
+ Legion::LLM::Discovery::Ollama.refresh! if Legion::LLM::Discovery::Ollama.stale?
191
+ discovered = Legion::LLM::Discovery::Ollama.model_names
192
+ models = discovered unless discovered.empty?
193
+ rescue StandardError
194
+ # fall back to default_model
195
+ end
196
+ end
197
+ result[name] = models
198
+ end
199
+ result
200
+ end
201
+
202
+ def ping_all_providers(out)
203
+ providers_cfg = llm_settings[:providers] || {}
204
+ enabled = providers_cfg.select { |_, c| c[:enabled] }
205
+
206
+ if enabled.empty?
207
+ out.warn('No providers enabled') unless options[:json]
208
+ return []
209
+ end
210
+
211
+ enabled.map do |name, cfg|
212
+ ping_one_provider(out, name, cfg)
213
+ end
214
+ end
215
+
216
+ def ping_one_provider(out, name, cfg)
217
+ model = cfg[:default_model]
218
+ return { provider: name, status: 'skip', message: 'no default model configured', latency_ms: nil } unless model
219
+
220
+ out.header(" Pinging #{name} (#{model})...") unless options[:json]
221
+ t0 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
222
+
223
+ response = RubyLLM.chat(model: model, provider: name).ask('Respond with only the word: pong')
224
+ elapsed = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - t0) * 1000).round
225
+
226
+ content = response.content.to_s.strip
227
+ success = content.downcase.include?('pong')
228
+
229
+ if success
230
+ out.success(" #{name}: pong (#{elapsed}ms)") unless options[:json]
231
+ else
232
+ out.warn(" #{name}: unexpected response (#{elapsed}ms): #{content[0..80]}") unless options[:json]
233
+ end
234
+
235
+ { provider: name, status: success ? 'ok' : 'unexpected', response: content[0..80],
236
+ model: model, latency_ms: elapsed }
237
+ rescue StandardError => e
238
+ elapsed = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - t0) * 1000).round if t0
239
+
240
+ out.error(" #{name}: #{e.message}") unless options[:json]
241
+ { provider: name, status: 'error', message: e.message, model: model, latency_ms: elapsed }
242
+ end
243
+
244
+ def show_status(out, data)
245
+ out.header('LLM Status')
246
+ out.detail({
247
+ 'Started' => data[:started].to_s,
248
+ 'Default Provider' => (data[:default_provider] || '(none)').to_s,
249
+ 'Default Model' => (data[:default_model] || '(none)').to_s,
250
+ 'Providers Enabled' => "#{data[:enabled_count]}/#{data[:total_count]}"
251
+ })
252
+
253
+ out.spacer
254
+ show_providers(out, data[:providers])
255
+
256
+ routing = data[:routing] || {}
257
+ if routing[:enabled]
258
+ out.spacer
259
+ out.header('Routing')
260
+ out.detail({
261
+ 'Enabled' => routing[:enabled].to_s,
262
+ 'Local Tier' => routing[:local_tier].to_s,
263
+ 'Fleet Tier' => routing[:fleet_tier].to_s,
264
+ 'Cloud Tier' => routing[:cloud_tier].to_s
265
+ })
266
+ end
267
+
268
+ sys = data[:system] || {}
269
+ return if sys.empty?
270
+
271
+ out.spacer
272
+ out.header('System')
273
+ out.detail({
274
+ 'Platform' => (sys[:platform] || 'unknown').to_s,
275
+ 'Total Memory' => sys[:total_memory_mb] ? "#{sys[:total_memory_mb]} MB" : 'unknown',
276
+ 'Available Memory' => sys[:avail_memory_mb] ? "#{sys[:avail_memory_mb]} MB" : 'unknown',
277
+ 'Memory Pressure' => sys[:memory_pressure].to_s
278
+ })
279
+ end
280
+
281
+ def show_providers(out, providers_data)
282
+ out.header('Providers')
283
+ providers_data.each do |p|
284
+ status = if p[:enabled]
285
+ reach = p[:reachable]
286
+ case reach
287
+ when true then 'enabled, reachable'
288
+ when :credentials_present then 'enabled, credentials present'
289
+ when false then 'enabled, unreachable'
290
+ else 'enabled'
291
+ end
292
+ else
293
+ 'disabled'
294
+ end
295
+
296
+ color = p[:enabled] ? :green : :muted
297
+ name_str = p[:name].to_s.ljust(12)
298
+ model_str = p[:default_model] ? " (#{p[:default_model]})" : ''
299
+ puts " #{out.colorize(name_str, :label)}#{out.colorize(status, color)}#{model_str}"
300
+ end
301
+ end
302
+
303
+ def show_models(out, models_data)
304
+ out.header('Available Models')
305
+ if models_data.empty?
306
+ out.warn('No providers enabled')
307
+ return
308
+ end
309
+
310
+ models_data.each do |provider, model_list|
311
+ out.spacer
312
+ puts " #{out.colorize(provider.to_s, :accent)} (#{model_list.size} model#{'s' unless model_list.size == 1})"
313
+ model_list.each { |m| puts " #{m}" }
314
+ end
315
+ end
316
+
317
+ def show_ping_results(out, results)
318
+ return if results.empty?
319
+
320
+ out.spacer
321
+ out.header('Ping Results')
322
+ passed = 0
323
+ failed = 0
324
+
325
+ results.each do |r|
326
+ case r[:status]
327
+ when 'ok'
328
+ passed += 1
329
+ when 'skip'
330
+ puts " #{out.colorize(r[:provider].to_s.ljust(12), :label)}#{out.colorize('skipped', :muted)} #{r[:message]}"
331
+ else
332
+ failed += 1
333
+ end
334
+ end
335
+
336
+ out.spacer
337
+ if failed.zero?
338
+ out.success("#{passed} provider(s) responding")
339
+ else
340
+ out.error("#{failed} provider(s) failed, #{passed} responding")
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end
@@ -33,6 +33,7 @@ module Legion
33
33
 
34
34
  before = snapshot_versions(target_gems)
35
35
  results = update_gems(target_gems, gem_bin, dry_run: options[:dry_run])
36
+ Gem::Specification.reset unless options[:dry_run]
36
37
  after = options[:dry_run] ? before : snapshot_versions(target_gems)
37
38
 
38
39
  if options[:json]
@@ -127,6 +128,21 @@ module Legion
127
128
  puts 'All gems are up to date'
128
129
  end
129
130
  out.error("#{failed.size} gem(s) failed to update") if failed.any?
131
+
132
+ suggest_detect(out)
133
+ end
134
+
135
+ def suggest_detect(out)
136
+ require 'legion/extensions/detect'
137
+ missing = Legion::Extensions::Detect.missing
138
+ return if missing.empty?
139
+
140
+ out.spacer
141
+ puts " #{missing.size} new extension(s) recommended based on your environment:"
142
+ missing.each { |name| puts " gem install #{name}" }
143
+ puts " Run 'legionio detect --install' to install them"
144
+ rescue LoadError
145
+ nil
130
146
  end
131
147
  end
132
148
  end
data/lib/legion/cli.rb CHANGED
@@ -35,12 +35,14 @@ module Legion
35
35
  autoload :Auth, 'legion/cli/auth_command'
36
36
  autoload :Rbac, 'legion/cli/rbac_command'
37
37
  autoload :Audit, 'legion/cli/audit_command'
38
+ autoload :Detect, 'legion/cli/detect_command'
38
39
  autoload :Update, 'legion/cli/update_command'
39
40
  autoload :Init, 'legion/cli/init_command'
40
41
  autoload :Skill, 'legion/cli/skill_command'
41
42
  autoload :Cost, 'legion/cli/cost_command'
42
43
  autoload :Marketplace, 'legion/cli/marketplace_command'
43
44
  autoload :Notebook, 'legion/cli/notebook_command'
45
+ autoload :Llm, 'legion/cli/llm_command'
44
46
  autoload :Tty, 'legion/cli/tty_command'
45
47
  autoload :Interactive, 'legion/cli/interactive'
46
48
 
@@ -59,7 +61,8 @@ module Legion
59
61
  def version
60
62
  out = formatter
61
63
  if options[:json]
62
- out.json(version: Legion::VERSION, ruby: RUBY_VERSION, platform: RUBY_PLATFORM)
64
+ out.json(version: Legion::VERSION, ruby: RUBY_VERSION, platform: RUBY_PLATFORM,
65
+ components: installed_components, extensions: discovered_lexs.size)
63
66
  else
64
67
  out.banner(version: Legion::VERSION)
65
68
  out.spacer
@@ -211,6 +214,9 @@ module Legion
211
214
  desc 'audit SUBCOMMAND', 'Audit log inspection and verification'
212
215
  subcommand 'audit', Legion::CLI::Audit
213
216
 
217
+ desc 'detect', 'Scan environment and recommend extensions'
218
+ subcommand 'detect', Legion::CLI::Detect
219
+
214
220
  desc 'update', 'Update Legion gems to latest versions'
215
221
  subcommand 'update', Legion::CLI::Update
216
222
 
@@ -229,6 +235,9 @@ module Legion
229
235
  desc 'notebook', 'Read and export Jupyter notebooks'
230
236
  subcommand 'notebook', Legion::CLI::Notebook
231
237
 
238
+ desc 'llm', 'LLM provider diagnostics (status, ping, models)'
239
+ subcommand 'llm', Legion::CLI::Llm
240
+
232
241
  desc 'tty', 'Rich terminal UI (onboarding, AI chat, dashboard)'
233
242
  subcommand 'tty', Legion::CLI::Tty
234
243
 
@@ -300,7 +309,8 @@ module Legion
300
309
 
301
310
  def installed_components
302
311
  components = { legionio: Legion::VERSION }
303
- %w[legion-transport legion-data legion-cache legion-crypt legion-json legion-logging legion-settings].each do |gem_name|
312
+ %w[legion-transport legion-data legion-cache legion-crypt legion-json legion-logging legion-settings
313
+ legion-llm legion-gaia legion-tty].each do |gem_name|
304
314
  spec = Gem::Specification.find_by_name(gem_name)
305
315
  short = gem_name.sub('legion-', '')
306
316
  components[short.to_sym] = spec.version.to_s
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.66'
4
+ VERSION = '1.4.68'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legionio
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.66
4
+ version: 1.4.68
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -430,6 +430,7 @@ files:
430
430
  - lib/legion/cli/dashboard/data_fetcher.rb
431
431
  - lib/legion/cli/dashboard/renderer.rb
432
432
  - lib/legion/cli/dashboard_command.rb
433
+ - lib/legion/cli/detect_command.rb
433
434
  - lib/legion/cli/doctor/bundle_check.rb
434
435
  - lib/legion/cli/doctor/cache_check.rb
435
436
  - lib/legion/cli/doctor/config_check.rb
@@ -483,6 +484,7 @@ files:
483
484
  - lib/legion/cli/lex/templates/runner_spec.erb
484
485
  - lib/legion/cli/lex_command.rb
485
486
  - lib/legion/cli/lex_templates.rb
487
+ - lib/legion/cli/llm_command.rb
486
488
  - lib/legion/cli/marketplace_command.rb
487
489
  - lib/legion/cli/mcp_command.rb
488
490
  - lib/legion/cli/memory_command.rb