legionio 1.4.101 → 1.4.103
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 +14 -0
- data/lib/legion/cli/image_command.rb +164 -0
- data/lib/legion/cli/team_command.rb +100 -0
- data/lib/legion/cli.rb +8 -0
- data/lib/legion/team/cost_attribution.rb +14 -0
- data/lib/legion/team.rb +26 -0
- data/lib/legion/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f036f54d059c1cdf1660a1ae08ed95b1fe97404d509123574508b9a6b36a1674
|
|
4
|
+
data.tar.gz: 616cacd65849dcdf73095f946df27c1afd9af68f9ee5f1b292ef53ee06f4554c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6681f51110762da3d76aa93b1e1ee2a92814c22e101842ef588f3a4d9492f585bdbf9bb4244df59c320c89d0dc992e17ba388b2ad56015a533fd7dec5411d378
|
|
7
|
+
data.tar.gz: ef6398f327e82cc91396d26fc855a6472eff22ee5f12e3ad225fbbbc06ee6df92912ef5c376dc9c0c838e0b6108bb3dbc7066f0372c820bb8e740d677fa70923
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.103] - 2026-03-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Legion::Team` module — team registry backed by settings (current, members, find, list)
|
|
7
|
+
- `Legion::Team::CostAttribution` — tags LLM request metadata with team and user context
|
|
8
|
+
- `legion team` CLI subcommand — list, show, current, set, create, add-member
|
|
9
|
+
|
|
10
|
+
## [1.4.102] - 2026-03-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `legion image analyze PATH` — analyze an image file via LLM; supports `--prompt`, `--model`, `--provider`, `--format text|json`
|
|
14
|
+
- `legion image compare PATH1 PATH2` — compare two images side by side via LLM with same options
|
|
15
|
+
- Supports png, jpg, jpeg, gif, webp; base64-encodes image data and builds multimodal content blocks for the LLM message
|
|
16
|
+
|
|
3
17
|
## [1.4.101] - 2026-03-21
|
|
4
18
|
|
|
5
19
|
### Fixed
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'base64'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module CLI
|
|
8
|
+
class Image < Thor
|
|
9
|
+
def self.exit_on_failure?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
SUPPORTED_TYPES = %w[png jpg jpeg gif webp].freeze
|
|
14
|
+
|
|
15
|
+
MIME_TYPES = {
|
|
16
|
+
'png' => 'image/png',
|
|
17
|
+
'jpg' => 'image/jpeg',
|
|
18
|
+
'jpeg' => 'image/jpeg',
|
|
19
|
+
'gif' => 'image/gif',
|
|
20
|
+
'webp' => 'image/webp'
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
|
|
24
|
+
class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
|
|
25
|
+
class_option :verbose, type: :boolean, default: false, aliases: ['-V'], desc: 'Verbose logging'
|
|
26
|
+
class_option :config_dir, type: :string, desc: 'Config directory path'
|
|
27
|
+
|
|
28
|
+
desc 'analyze PATH', 'Analyze an image file using an LLM'
|
|
29
|
+
option :prompt, type: :string, aliases: ['-p'],
|
|
30
|
+
desc: 'Custom question to ask about the image',
|
|
31
|
+
default: 'Describe this image in detail'
|
|
32
|
+
option :model, type: :string, aliases: ['-m'], desc: 'LLM model override'
|
|
33
|
+
option :provider, type: :string, desc: 'LLM provider override'
|
|
34
|
+
option :format, type: :string, default: 'text', desc: 'Output format: text or json'
|
|
35
|
+
def analyze(path)
|
|
36
|
+
out = formatter
|
|
37
|
+
setup_connection(out)
|
|
38
|
+
|
|
39
|
+
image_data = load_image(path, out)
|
|
40
|
+
return unless image_data
|
|
41
|
+
|
|
42
|
+
messages = [build_image_message([image_data], options[:prompt])]
|
|
43
|
+
response = call_llm(messages, out)
|
|
44
|
+
return unless response
|
|
45
|
+
|
|
46
|
+
render_response(out, response, { path: path, prompt: options[:prompt] })
|
|
47
|
+
rescue CLI::Error => e
|
|
48
|
+
formatter.error(e.message)
|
|
49
|
+
raise SystemExit, 1
|
|
50
|
+
ensure
|
|
51
|
+
Connection.shutdown
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc 'compare PATH1 PATH2', 'Compare two images side by side using an LLM'
|
|
55
|
+
option :prompt, type: :string, aliases: ['-p'],
|
|
56
|
+
desc: 'Custom comparison question',
|
|
57
|
+
default: 'Compare these two images and describe the differences'
|
|
58
|
+
option :model, type: :string, aliases: ['-m'], desc: 'LLM model override'
|
|
59
|
+
option :provider, type: :string, desc: 'LLM provider override'
|
|
60
|
+
option :format, type: :string, default: 'text', desc: 'Output format: text or json'
|
|
61
|
+
def compare(path1, path2)
|
|
62
|
+
out = formatter
|
|
63
|
+
setup_connection(out)
|
|
64
|
+
|
|
65
|
+
image1 = load_image(path1, out)
|
|
66
|
+
return unless image1
|
|
67
|
+
|
|
68
|
+
image2 = load_image(path2, out)
|
|
69
|
+
return unless image2
|
|
70
|
+
|
|
71
|
+
messages = [build_image_message([image1, image2], options[:prompt])]
|
|
72
|
+
response = call_llm(messages, out)
|
|
73
|
+
return unless response
|
|
74
|
+
|
|
75
|
+
render_response(out, response, { path1: path1, path2: path2, prompt: options[:prompt] })
|
|
76
|
+
rescue CLI::Error => e
|
|
77
|
+
formatter.error(e.message)
|
|
78
|
+
raise SystemExit, 1
|
|
79
|
+
ensure
|
|
80
|
+
Connection.shutdown
|
|
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
|
+
def setup_connection(out)
|
|
92
|
+
Connection.config_dir = options[:config_dir] if options[:config_dir]
|
|
93
|
+
Connection.log_level = options[:verbose] ? 'debug' : 'error'
|
|
94
|
+
Connection.ensure_llm
|
|
95
|
+
rescue CLI::Error => e
|
|
96
|
+
out.error(e.message)
|
|
97
|
+
raise SystemExit, 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def load_image(path, out)
|
|
101
|
+
unless File.exist?(path)
|
|
102
|
+
out.error("File not found: #{path}")
|
|
103
|
+
raise SystemExit, 1
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
ext = File.extname(path).delete_prefix('.').downcase
|
|
107
|
+
unless SUPPORTED_TYPES.include?(ext)
|
|
108
|
+
out.error("Unsupported image type '.#{ext}'. Supported: #{SUPPORTED_TYPES.join(', ')}")
|
|
109
|
+
raise SystemExit, 1
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
path: path,
|
|
114
|
+
mime_type: MIME_TYPES[ext],
|
|
115
|
+
data: Base64.strict_encode64(File.binread(path))
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_image_message(images, prompt_text)
|
|
120
|
+
content = images.map do |img|
|
|
121
|
+
{
|
|
122
|
+
type: 'image',
|
|
123
|
+
source: {
|
|
124
|
+
type: 'base64',
|
|
125
|
+
media_type: img[:mime_type],
|
|
126
|
+
data: img[:data]
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
content << { type: 'text', text: prompt_text }
|
|
131
|
+
{ role: 'user', content: content }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def call_llm(messages, out)
|
|
135
|
+
llm_kwargs = {}
|
|
136
|
+
llm_kwargs[:model] = options[:model] if options[:model]
|
|
137
|
+
llm_kwargs[:provider] = options[:provider].to_sym if options[:provider]
|
|
138
|
+
|
|
139
|
+
Legion::LLM.chat(messages: messages, **llm_kwargs)
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
out.error("LLM call failed: #{e.message}")
|
|
142
|
+
raise SystemExit, 1
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def render_response(out, response, meta)
|
|
146
|
+
content = response[:content].to_s
|
|
147
|
+
usage = response[:usage] || {}
|
|
148
|
+
|
|
149
|
+
if options[:format] == 'json' || options[:json]
|
|
150
|
+
out.json(meta.merge(response: content, usage: usage))
|
|
151
|
+
else
|
|
152
|
+
out.header('Analysis')
|
|
153
|
+
out.spacer
|
|
154
|
+
puts content
|
|
155
|
+
return if usage.nil? || usage.empty?
|
|
156
|
+
|
|
157
|
+
out.spacer
|
|
158
|
+
out.detail(usage)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module CLI
|
|
7
|
+
class Team < Thor
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc 'list', 'List all teams'
|
|
13
|
+
def list
|
|
14
|
+
require 'legion/settings'
|
|
15
|
+
require 'legion/team'
|
|
16
|
+
teams = Legion::Team.list
|
|
17
|
+
if teams.empty?
|
|
18
|
+
say 'No teams configured.', :yellow
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
say 'Teams', :green
|
|
22
|
+
say '-' * 20
|
|
23
|
+
teams.each { |t| say " #{t}" }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc 'show TEAM', 'Show team details and members'
|
|
27
|
+
def show(name)
|
|
28
|
+
require 'legion/settings'
|
|
29
|
+
require 'legion/team'
|
|
30
|
+
team = Legion::Team.find(name)
|
|
31
|
+
if team.nil?
|
|
32
|
+
say "Team '#{name}' not found.", :red
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
say "Team: #{name}", :green
|
|
36
|
+
say '-' * 20
|
|
37
|
+
members = team[:members] || []
|
|
38
|
+
if members.empty?
|
|
39
|
+
say ' No members.'
|
|
40
|
+
else
|
|
41
|
+
members.each { |m| say " #{m}" }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc 'current', 'Show the current active team'
|
|
46
|
+
def current
|
|
47
|
+
require 'legion/settings'
|
|
48
|
+
require 'legion/team'
|
|
49
|
+
say Legion::Team.current
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc 'set TEAM', 'Set the active team in settings'
|
|
53
|
+
def set(name)
|
|
54
|
+
require 'legion/settings'
|
|
55
|
+
require 'legion/team'
|
|
56
|
+
Legion::Settings.load unless Legion::Settings.instance_variable_get(:@loader)
|
|
57
|
+
Legion::Settings.loader.settings[:team] ||= {}
|
|
58
|
+
Legion::Settings.loader.settings[:team][:name] = name
|
|
59
|
+
say "Active team set to '#{name}'.", :green
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
desc 'create TEAM', 'Create a new team'
|
|
63
|
+
def create(name)
|
|
64
|
+
require 'legion/settings'
|
|
65
|
+
require 'legion/team'
|
|
66
|
+
Legion::Settings.load unless Legion::Settings.instance_variable_get(:@loader)
|
|
67
|
+
teams = Legion::Settings.loader.settings[:teams] || {}
|
|
68
|
+
if teams.key?(name.to_sym)
|
|
69
|
+
say "Team '#{name}' already exists.", :yellow
|
|
70
|
+
return
|
|
71
|
+
end
|
|
72
|
+
teams[name.to_sym] = { name: name, members: [] }
|
|
73
|
+
Legion::Settings.loader.settings[:teams] = teams
|
|
74
|
+
say "Team '#{name}' created.", :green
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc 'add-member TEAM USER', 'Add a member to a team'
|
|
78
|
+
map 'add-member' => :add_member
|
|
79
|
+
def add_member(team_name, user)
|
|
80
|
+
require 'legion/settings'
|
|
81
|
+
require 'legion/team'
|
|
82
|
+
Legion::Settings.load unless Legion::Settings.instance_variable_get(:@loader)
|
|
83
|
+
teams = Legion::Settings.loader.settings[:teams] || {}
|
|
84
|
+
sym = team_name.to_sym
|
|
85
|
+
unless teams.key?(sym)
|
|
86
|
+
say "Team '#{team_name}' not found.", :red
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
teams[sym][:members] ||= []
|
|
90
|
+
if teams[sym][:members].include?(user)
|
|
91
|
+
say "#{user} is already a member of '#{team_name}'.", :yellow
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
teams[sym][:members] << user
|
|
95
|
+
Legion::Settings.loader.settings[:teams] = teams
|
|
96
|
+
say "Added #{user} to team '#{team_name}'.", :green
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/legion/cli.rb
CHANGED
|
@@ -42,8 +42,10 @@ module Legion
|
|
|
42
42
|
autoload :Init, 'legion/cli/init_command'
|
|
43
43
|
autoload :Skill, 'legion/cli/skill_command'
|
|
44
44
|
autoload :Prompt, 'legion/cli/prompt_command'
|
|
45
|
+
autoload :Image, 'legion/cli/image_command'
|
|
45
46
|
autoload :Dataset, 'legion/cli/dataset_command'
|
|
46
47
|
autoload :Cost, 'legion/cli/cost_command'
|
|
48
|
+
autoload :Team, 'legion/cli/team_command'
|
|
47
49
|
autoload :Marketplace, 'legion/cli/marketplace_command'
|
|
48
50
|
autoload :Notebook, 'legion/cli/notebook_command'
|
|
49
51
|
autoload :Llm, 'legion/cli/llm_command'
|
|
@@ -249,6 +251,9 @@ module Legion
|
|
|
249
251
|
desc 'cost', 'Cost visibility and reporting'
|
|
250
252
|
subcommand 'cost', Legion::CLI::Cost
|
|
251
253
|
|
|
254
|
+
desc 'team SUBCOMMAND', 'Team and multi-user management'
|
|
255
|
+
subcommand 'team', Legion::CLI::Team
|
|
256
|
+
|
|
252
257
|
desc 'marketplace', 'Extension marketplace (search, info, scan)'
|
|
253
258
|
subcommand 'marketplace', Legion::CLI::Marketplace
|
|
254
259
|
|
|
@@ -267,6 +272,9 @@ module Legion
|
|
|
267
272
|
desc 'observe SUBCOMMAND', 'MCP tool observation stats'
|
|
268
273
|
subcommand 'observe', Legion::CLI::ObserveCommand
|
|
269
274
|
|
|
275
|
+
desc 'image SUBCOMMAND', 'Multimodal image analysis and comparison'
|
|
276
|
+
subcommand 'image', Legion::CLI::Image
|
|
277
|
+
|
|
270
278
|
desc 'payroll SUBCOMMAND', 'Workforce cost and labor economics'
|
|
271
279
|
subcommand 'payroll', Legion::CLI::Payroll
|
|
272
280
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Team
|
|
5
|
+
module CostAttribution
|
|
6
|
+
def self.tag(metadata = {})
|
|
7
|
+
metadata.merge(
|
|
8
|
+
team: Legion::Team.current,
|
|
9
|
+
user: Legion::Settings.dig(:team, :user) || ENV.fetch('USER', nil)
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/legion/team.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/team/cost_attribution'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
module Team
|
|
7
|
+
class << self
|
|
8
|
+
def current
|
|
9
|
+
Legion::Settings.dig(:team, :name) || 'default'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def members
|
|
13
|
+
Legion::Settings.dig(:team, :members) || []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def find(name)
|
|
17
|
+
teams = Legion::Settings[:teams] || {}
|
|
18
|
+
teams[name.to_sym]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def list
|
|
22
|
+
(Legion::Settings[:teams] || {}).keys.map(&:to_s)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
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.103
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -472,6 +472,7 @@ files:
|
|
|
472
472
|
- lib/legion/cli/gaia_command.rb
|
|
473
473
|
- lib/legion/cli/generate_command.rb
|
|
474
474
|
- lib/legion/cli/graph_command.rb
|
|
475
|
+
- lib/legion/cli/image_command.rb
|
|
475
476
|
- lib/legion/cli/init/config_generator.rb
|
|
476
477
|
- lib/legion/cli/init/environment_detector.rb
|
|
477
478
|
- lib/legion/cli/init_command.rb
|
|
@@ -547,6 +548,7 @@ files:
|
|
|
547
548
|
- lib/legion/cli/swarm_command.rb
|
|
548
549
|
- lib/legion/cli/task.rb
|
|
549
550
|
- lib/legion/cli/task_command.rb
|
|
551
|
+
- lib/legion/cli/team_command.rb
|
|
550
552
|
- lib/legion/cli/telemetry_command.rb
|
|
551
553
|
- lib/legion/cli/templates/core.json.erb
|
|
552
554
|
- lib/legion/cli/theme.rb
|
|
@@ -617,6 +619,8 @@ files:
|
|
|
617
619
|
- lib/legion/sandbox.rb
|
|
618
620
|
- lib/legion/service.rb
|
|
619
621
|
- lib/legion/supervision.rb
|
|
622
|
+
- lib/legion/team.rb
|
|
623
|
+
- lib/legion/team/cost_attribution.rb
|
|
620
624
|
- lib/legion/telemetry.rb
|
|
621
625
|
- lib/legion/telemetry/open_inference.rb
|
|
622
626
|
- lib/legion/telemetry/safety_metrics.rb
|