legionio 1.4.67 → 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: a91ac55fb8db8f7105267bc77653cfd6c1a834a702fe6197a0e3687a889c841b
4
- data.tar.gz: 7819aaa4e4dadb1e12e7f8741f57549ca6e8e801cc22aa472835c95cc16332df
3
+ metadata.gz: 1521235e6e865bd6d74f41b93c57e159de23d9dee0f044486c8059a6af780ab0
4
+ data.tar.gz: 7bdb02eb37ca2f03b9c0b6f7f494ebbb394d69f53ff83dd4bb5c1e27cb37e85e
5
5
  SHA512:
6
- metadata.gz: a3d119331cd7741a3a5ec70b31ff224be0c74732334f200a38e4392eefd04cf1dc6846fc34a712ac6c6512f6baf45de9a83e26b8d0be723b14ab23efe05c1797
7
- data.tar.gz: 3d2b1ff8e85f33cbcbbaceb60d8f60bfc176d23abb6c85c5212fc0ce810aeb588a964b51efef38832c049aa16efd940944da0f872be4e4178f5b611836a11151
6
+ metadata.gz: 78a7171285d6a6eebda11d87d2782a68b6edb0dbc1f0be6d6034c1b4c7e535a5ae2dc1feb34ecbcc5af64d767759c7af7c78c1c50ed8672a1f08dea738c6b23b
7
+ data.tar.gz: 0b7c0e4d1298e13815383df8667d2b14debf66675c981002847fa19a01d0be6afa4314d811ba7cde7285ee46ddf37e9985e8a86745aef2b4b122d2a7dc1e16ac
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
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
+
3
18
  ## [1.4.67] - 2026-03-18
4
19
 
5
20
  ### Added
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,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]
data/lib/legion/cli.rb CHANGED
@@ -42,6 +42,7 @@ module Legion
42
42
  autoload :Cost, 'legion/cli/cost_command'
43
43
  autoload :Marketplace, 'legion/cli/marketplace_command'
44
44
  autoload :Notebook, 'legion/cli/notebook_command'
45
+ autoload :Llm, 'legion/cli/llm_command'
45
46
  autoload :Tty, 'legion/cli/tty_command'
46
47
  autoload :Interactive, 'legion/cli/interactive'
47
48
 
@@ -60,7 +61,8 @@ module Legion
60
61
  def version
61
62
  out = formatter
62
63
  if options[:json]
63
- 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)
64
66
  else
65
67
  out.banner(version: Legion::VERSION)
66
68
  out.spacer
@@ -233,6 +235,9 @@ module Legion
233
235
  desc 'notebook', 'Read and export Jupyter notebooks'
234
236
  subcommand 'notebook', Legion::CLI::Notebook
235
237
 
238
+ desc 'llm', 'LLM provider diagnostics (status, ping, models)'
239
+ subcommand 'llm', Legion::CLI::Llm
240
+
236
241
  desc 'tty', 'Rich terminal UI (onboarding, AI chat, dashboard)'
237
242
  subcommand 'tty', Legion::CLI::Tty
238
243
 
@@ -304,7 +309,8 @@ module Legion
304
309
 
305
310
  def installed_components
306
311
  components = { legionio: Legion::VERSION }
307
- %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|
308
314
  spec = Gem::Specification.find_by_name(gem_name)
309
315
  short = gem_name.sub('legion-', '')
310
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.67'
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.67
4
+ version: 1.4.68
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -484,6 +484,7 @@ files:
484
484
  - lib/legion/cli/lex/templates/runner_spec.erb
485
485
  - lib/legion/cli/lex_command.rb
486
486
  - lib/legion/cli/lex_templates.rb
487
+ - lib/legion/cli/llm_command.rb
487
488
  - lib/legion/cli/marketplace_command.rb
488
489
  - lib/legion/cli/mcp_command.rb
489
490
  - lib/legion/cli/memory_command.rb