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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a5e67604f3f5f8bf4b49bea289800d891fc4073ac2947adef44760cf1eca2ff
4
- data.tar.gz: af9fc49a94f6babfde4a69905d3c1da3c3d4cac2856ba71429a4720daadc0334
3
+ metadata.gz: f036f54d059c1cdf1660a1ae08ed95b1fe97404d509123574508b9a6b36a1674
4
+ data.tar.gz: 616cacd65849dcdf73095f946df27c1afd9af68f9ee5f1b292ef53ee06f4554c
5
5
  SHA512:
6
- metadata.gz: 2e64d067a8e1dcc4005b2c7640aaf04112b94b0499a661003a8bcd56b80023e7543a6bfcfd81f3642f74fe041ef77bcb3a8dead6216873f94329e3e2c72b32fd
7
- data.tar.gz: ffe6519b87c9ff5e891ddfeb8f785ace26729f48e840c3bc5fad409dae8b2997b5cd6b074bfb6171447625ea72b1f11a4a31a23747c187171c2c8b750ff7fe21
6
+ metadata.gz: 6681f51110762da3d76aa93b1e1ee2a92814c22e101842ef588f3a4d9492f585bdbf9bb4244df59c320c89d0dc992e17ba388b2ad56015a533fd7dec5411d378
7
+ data.tar.gz: ef6398f327e82cc91396d26fc855a6472eff22ee5f12e3ad225fbbbc06ee6df92912ef5c376dc9c0c838e0b6108bb3dbc7066f0372c820bb8e740d677fa70923
data/.rubocop.yml CHANGED
@@ -42,6 +42,7 @@ Metrics/BlockLength:
42
42
  - 'lib/legion/cli/auth_command.rb'
43
43
  - 'lib/legion/cli/detect_command.rb'
44
44
  - 'lib/legion/cli/prompt_command.rb'
45
+ - 'lib/legion/cli/image_command.rb'
45
46
  - 'lib/legion/api/acp.rb'
46
47
 
47
48
  Metrics/AbcSize:
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.101'
4
+ VERSION = '1.4.103'
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.101
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