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 +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +26 -0
- data/CLAUDE.md +3 -1
- data/README.md +3 -2
- data/lib/legion/cli/detect_command.rb +155 -0
- data/lib/legion/cli/llm_command.rb +346 -0
- data/lib/legion/cli/update_command.rb +16 -0
- data/lib/legion/cli.rb +12 -2
- data/lib/legion/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1521235e6e865bd6d74f41b93c57e159de23d9dee0f044486c8059a6af780ab0
|
|
4
|
+
data.tar.gz: 7bdb02eb37ca2f03b9c0b6f7f494ebbb394d69f53ff83dd4bb5c1e27cb37e85e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78a7171285d6a6eebda11d87d2782a68b6edb0dbc1f0be6d6034c1b4c7e535a5ae2dc1feb34ecbcc5af64d767759c7af7c78c1c50ed8672a1f08dea738c6b23b
|
|
7
|
+
data.tar.gz: 0b7c0e4d1298e13815383df8667d2b14debf66675c981002847fa19a01d0be6afa4314d811ba7cde7285ee46ddf37e9985e8a86745aef2b4b122d2a7dc1e16ac
|
data/.rubocop.yml
CHANGED
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.
|
|
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.
|
|
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 #
|
|
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
|
|
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
|
data/lib/legion/version.rb
CHANGED
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.
|
|
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
|