legionio 1.6.13 → 1.6.18

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: 8682edaf8eaf214c03e6dca287e4298260dde6482506bacee7c6ef247ad1fc8f
4
- data.tar.gz: 3f045eec756ee09f7d15ea25c7d40709fd95dd71d6e277845e400c4c6be19f98
3
+ metadata.gz: 5796d0724836fec4f8426fa337d5c6de420fbdf81c94fa9d070ca2a1a6abd9c7
4
+ data.tar.gz: bbaa73b5ee1d36c1c8b7cb9536d1c7e4144464cacfe68c1b4fee55b9bfea08df
5
5
  SHA512:
6
- metadata.gz: b41ad3b698ae6318ae2a6e7f1c3e0f1d34a46df6dcc183bcf644f5850d458c0d5c1ca080ddee2f81d69dce7a8d5ecdcbb7ae509295a7fd56f852e8f5af8aa189
7
- data.tar.gz: 860b776fc04f33f031f07c1969058f4278b8c6a124c09920a54c40b808bdbf708454b8062be32d3960741e8cef5afd1c2c38bb546ad382dd98e528c806a46bf4
6
+ metadata.gz: 2c1a641e20d067fddcf0d949c3d2f11bbbcca141783b20e581f361bb916821c07e3538037e00c21f7866a12399d3a2c3ce56ad040f8af5713ff2969ea4ee1463
7
+ data.tar.gz: 568b3efc39291f9e2593b373eb3009db4d708fc7030b38237924d52acf2933c5a64e0aafb4e39265fe8997a7fd7a66aded647d969a4dcb3a8af1e3207afe0c29
@@ -4,33 +4,6 @@ on:
4
4
  branches: [main]
5
5
 
6
6
  jobs:
7
- test:
8
- name: Test
9
- runs-on: ubuntu-latest
10
- services:
11
- rabbitmq:
12
- image: rabbitmq:3.13-management
13
- ports: ['5672:5672']
14
- postgres:
15
- image: postgres:16
16
- env:
17
- POSTGRES_PASSWORD: test
18
- POSTGRES_DB: legion_test
19
- ports: ['5432:5432']
20
- options: >-
21
- --health-cmd pg_isready
22
- --health-interval 10s
23
- --health-timeout 5s
24
- --health-retries 5
25
- steps:
26
- - uses: actions/checkout@v4
27
- - uses: ruby/setup-ruby@v1
28
- with:
29
- ruby-version: '3.4'
30
- bundler-cache: true
31
- - run: bundle install && bundle exec rspec
32
- - run: bundle exec rubocop
33
-
34
7
  helm-lint:
35
8
  name: Helm Lint
36
9
  runs-on: ubuntu-latest
data/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.6.18] - 2026-03-27
6
+
7
+ ### Fixed
8
+ - `legionio pipeline image analyze`: `call_llm` no longer passes unsupported `messages:` keyword to `Legion::LLM.chat`; now creates a chat object and sends multimodal content via `chat.ask`, returning a plain hash with `:content` and `:usage` keys
9
+ - `legionio ai trace search/summarize`: both commands now call `setup_connection` before invoking `TraceSearch`, ensuring `Legion::LLM` is booted so `TraceSearch.generate_filter` can use structured LLM output instead of returning "no filter generated"; added `class_option :config_dir` and `class_option :verbose` to `TraceCommand`
10
+
11
+ ## [1.6.17] - 2026-03-27
12
+
13
+ ### Fixed
14
+ - `legionio check`: `resolve_secrets!` is now called after a successful crypt check so `lease://`, `vault://`, and `env://` credential URIs are resolved before transport/data checks attempt to connect
15
+ - `legionio check transport`: raises an early descriptive error when transport credentials are still unresolved URI references (Vault lease pending), instead of failing with a confusing connection error
16
+ - `legionio check data`: raises an early descriptive error when database credentials are still unresolved URI references (Vault lease pending)
17
+ - `legionio llm status/providers/models`: `boot_llm_settings` now calls `resolve_secrets!` so `env://` and `vault://` API key references are resolved before provider enabled-state is evaluated
18
+ - `legionio llm providers`: providers with unresolved credential URIs are now shown as `deferred (credentials pending Vault)` in yellow instead of incorrectly `disabled`
19
+ - `Connection.ensure_settings`: calls `resolve_secrets!` after loading settings so `env://` references are resolved in all CLI commands that use the lazy connection manager
20
+
21
+ ## [1.6.16] - 2026-03-27
22
+
23
+ ### Fixed
24
+ - `config validate` transport host check now reads from `transport.connection.host` instead of `transport.host` (correct config nesting)
25
+ - `doctor diagnose` now loads settings via `Connection.ensure_settings` before running checks, so cache/database/vault/extensions checks no longer skip due to `Legion::Settings` being undefined; also adds `ensure Connection.shutdown` for clean teardown
26
+
27
+ ## [1.6.15] - 2026-03-27
28
+
29
+ ### Added
30
+ - Absorbers: new LEX component type for pattern-matched content acquisition
31
+ - `Absorbers::Base` class with `pattern`/`description` DSL and knowledge helpers (`absorb_to_knowledge`, `absorb_raw`, `translate`, `report_progress`)
32
+ - `Absorbers::Matchers::Base` auto-registering matcher interface with `Matchers::Url` for URL glob matching
33
+ - `Absorbers::PatternMatcher` for thread-safe input-to-absorber resolution with priority-based dispatch
34
+ - `Builders::Absorbers` for auto-discovery of absorber classes during extension boot
35
+ - `Capability.from_absorber` factory method for Capability Registry integration
36
+ - `AbsorberDispatch` module for pattern resolution and handler execution
37
+ - `legionio absorb` CLI command with `url`, `list`, and `resolve` subcommands
38
+ - `legionio dev generate absorber` scaffolding template
39
+
40
+ ## [1.6.14] - 2026-03-27
41
+
42
+ ### Added
43
+ - `Legion::Compliance` module rewritten with DEFAULTS hash, `merge_settings` registration, and clean API
44
+ - `Compliance.setup` registers max-classification defaults: PHI, PCI, PII, FedRAMP all enabled by default
45
+ - `Compliance.enabled?`, `.phi_enabled?`, `.pci_enabled?`, `.pii_enabled?`, `.fedramp_enabled?` convenience methods
46
+ - `Compliance.classification_level` returns `'confidential'` by default (highest level)
47
+ - `Compliance.profile` returns a hash with all compliance flags for downstream consumers
48
+ - `setup_compliance` wired into Service boot sequence after settings load
49
+ - Compliance profile spec (8 examples)
50
+
51
+ ### Changed
52
+ - `Compliance.phi_enabled?` now uses `Settings.dig(:compliance, :phi_enabled)` instead of chaining `[]` calls
53
+ - Existing PhiTag and PhiAccessLog specs updated to use `merge_settings` instead of stubbing `Settings.[]`
54
+
5
55
  ## [1.6.13] - 2026-03-27
6
56
 
7
57
  ### Added
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/absorbers'
4
+ require 'legion/extensions/absorbers/pattern_matcher'
5
+ require 'legion/extensions/actors/absorber_dispatch'
6
+
7
+ module Legion
8
+ module CLI
9
+ class AbsorbCommand < Thor
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
+
17
+ desc 'url URL', 'Absorb content from a URL'
18
+ option :scope, type: :string, default: 'global', desc: 'Knowledge scope (global/local/all)'
19
+ def url(input_url)
20
+ Connection.ensure_settings
21
+ out = formatter
22
+ result = Legion::Extensions::Actors::AbsorberDispatch.dispatch(
23
+ input: input_url,
24
+ context: { scope: options[:scope]&.to_sym }
25
+ )
26
+
27
+ if options[:json]
28
+ out.json(result)
29
+ elsif result[:success]
30
+ out.success("Absorbed: #{input_url}")
31
+ out.detail(absorber: result[:absorber], job_id: result[:job_id])
32
+ else
33
+ out.warn("Failed: #{result[:error]}")
34
+ end
35
+ end
36
+
37
+ desc 'list', 'List registered absorber patterns'
38
+ def list
39
+ Connection.ensure_settings
40
+ out = formatter
41
+ patterns = Legion::Extensions::Absorbers::PatternMatcher.list
42
+
43
+ if options[:json]
44
+ out.json(patterns.map { |p| { type: p[:type], value: p[:value], description: p[:description] } })
45
+ elsif patterns.empty?
46
+ out.warn('No absorbers registered')
47
+ else
48
+ headers = %w[Type Pattern Description]
49
+ rows = patterns.map do |p|
50
+ [p[:type].to_s, p[:value], p[:description] || '']
51
+ end
52
+ out.header('Registered Absorbers')
53
+ out.table(headers, rows)
54
+ end
55
+ end
56
+
57
+ desc 'resolve URL', 'Show which absorber would handle a URL (dry run)'
58
+ def resolve(input_url)
59
+ Connection.ensure_settings
60
+ out = formatter
61
+ absorber = Legion::Extensions::Absorbers::PatternMatcher.resolve(input_url)
62
+
63
+ if options[:json]
64
+ out.json({ input: input_url, absorber: absorber&.name, match: !absorber.nil? })
65
+ elsif absorber
66
+ out.success("#{input_url} -> #{absorber.name}")
67
+ else
68
+ out.warn("No absorber registered for: #{input_url}")
69
+ end
70
+ end
71
+
72
+ no_commands do
73
+ def formatter
74
+ @formatter ||= Output::Formatter.new(json: options[:json], color: !options[:no_color])
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -18,9 +18,20 @@ module Legion
18
18
  def execute(command:, timeout: 120, working_directory: nil)
19
19
  dir = working_directory ? File.expand_path(working_directory) : Dir.pwd
20
20
 
21
- stdout, stderr, status = nil
22
- ::Timeout.timeout(timeout) do
23
- stdout, stderr, status = Open3.capture3(command, chdir: dir)
21
+ stdout, stderr, status = Open3.popen3(command, chdir: dir) do |stdin, out, err, wait_thr|
22
+ stdin.close
23
+ out_reader = Thread.new { out.read }
24
+ err_reader = Thread.new { err.read }
25
+
26
+ unless wait_thr.join(timeout)
27
+ ::Process.kill('TERM', wait_thr.pid)
28
+ wait_thr.join(5) || ::Process.kill('KILL', wait_thr.pid)
29
+ out_reader.kill
30
+ err_reader.kill
31
+ raise ::Timeout::Error, "command timed out after #{timeout}s"
32
+ end
33
+
34
+ [out_reader.value, err_reader.value, wait_thr.value]
24
35
  end
25
36
 
26
37
  output = String.new
@@ -29,11 +40,9 @@ module Legion
29
40
  output << stderr unless stderr.empty?
30
41
  output << "\n[exit code: #{status.exitstatus}]"
31
42
  output
32
- rescue ::Timeout::Error => e
33
- Legion::Logging.warn("RunCommand#execute timed out after #{timeout}s for command #{command}: #{e.message}") if defined?(Legion::Logging)
43
+ rescue ::Timeout::Error
34
44
  "[command timed out after #{timeout}s]: #{command}"
35
45
  rescue StandardError => e
36
- Legion::Logging.warn("RunCommand#execute failed for command #{command}: #{e.message}") if defined?(Legion::Logging)
37
46
  "Error executing command: #{e.message}"
38
47
  end
39
48
  end
@@ -107,6 +107,7 @@ module Legion
107
107
 
108
108
  results[name] = run_check(name, options)
109
109
  started << name if results[name][:status] == 'pass'
110
+ resolve_secrets_after_crypt(name, results[name])
110
111
  print_result(formatter, name, results[name], options) unless options[:json]
111
112
  end
112
113
 
@@ -123,6 +124,15 @@ module Legion
123
124
  Legion::Logging.setup(log_level: log_level, level: log_level, trace: false)
124
125
  end
125
126
 
127
+ def resolve_secrets_after_crypt(name, result)
128
+ return unless name == :crypt && result[:status] == 'pass'
129
+ return unless Legion::Settings.respond_to?(:resolve_secrets!)
130
+
131
+ Legion::Settings.resolve_secrets!
132
+ rescue StandardError => e
133
+ Legion::Logging.warn("Check#run secret resolution failed: #{e.message}") if defined?(Legion::Logging)
134
+ end
135
+
126
136
  def run_check(name, options)
127
137
  start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
128
138
  detail = send(:"check_#{name}", options)
@@ -151,6 +161,15 @@ module Legion
151
161
  def check_transport(_options)
152
162
  require 'legion/transport'
153
163
  Legion::Settings.merge_settings('transport', Legion::Transport::Settings.default)
164
+ conn = Legion::Settings[:transport][:connection] || {}
165
+ user = conn[:user].to_s
166
+ pass = conn[:password].to_s
167
+ if user.start_with?('lease://', 'vault://') || pass.start_with?('lease://', 'vault://')
168
+ scheme = user[%r{\A[^:]+://}]
169
+ redacted = scheme ? "#{scheme}..." : '(unresolved)'
170
+ raise "credentials not resolved (Vault lease pending) — user: #{redacted}"
171
+ end
172
+
154
173
  Legion::Transport::Connection.setup
155
174
  if Legion::Transport::Connection.lite_mode?
156
175
  'InProcess (lite mode)'
@@ -189,6 +208,11 @@ module Legion
189
208
  def check_data(_options)
190
209
  require 'legion/data'
191
210
  Legion::Settings.merge_settings(:data, Legion::Data::Settings.default)
211
+ creds = Legion::Settings[:data][:creds] || Legion::Settings[:data] || {}
212
+ db_user = (creds[:user] || creds[:username]).to_s
213
+ db_pass = creds[:password].to_s
214
+ raise_if_unresolved_data_creds(db_user, db_pass)
215
+
192
216
  Legion::Data.setup
193
217
  ds = Legion::Settings[:data] || {}
194
218
  adapter = ds[:adapter] || 'sqlite'
@@ -203,6 +227,20 @@ module Legion
203
227
  end
204
228
  end
205
229
 
230
+ def raise_if_unresolved_data_creds(db_user, db_pass)
231
+ return unless db_user.start_with?('lease://', 'vault://') || db_pass.start_with?('lease://', 'vault://')
232
+
233
+ unresolved_fields = []
234
+ unresolved_fields << 'user' if db_user.start_with?('lease://', 'vault://')
235
+ unresolved_fields << 'password' if db_pass.start_with?('lease://', 'vault://')
236
+ scheme_hints = []
237
+ scheme_hints << 'lease://...' if db_user.start_with?('lease://') || db_pass.start_with?('lease://')
238
+ scheme_hints << 'vault://...' if db_user.start_with?('vault://') || db_pass.start_with?('vault://')
239
+ details = "unresolved fields: #{unresolved_fields.join(', ')}"
240
+ details += " (#{scheme_hints.join(', ')})" unless scheme_hints.empty?
241
+ raise "credentials not resolved (Vault lease pending) — #{details}"
242
+ end
243
+
206
244
  def check_data_local(_options)
207
245
  if defined?(Legion::Data::Local) && Legion::Data::Local.respond_to?(:setup)
208
246
  Legion::Data::Local.setup unless Legion::Data::Local.respond_to?(:connected?) && Legion::Data::Local.connected?
@@ -119,7 +119,8 @@ module Legion
119
119
  # Check transport config
120
120
  if Connection.settings?
121
121
  transport = Legion::Settings[:transport] || {}
122
- warnings << 'Transport host not configured (RabbitMQ will use default localhost)' if transport[:host].nil? || transport[:host].to_s.empty?
122
+ transport_host = transport.dig(:connection, :host)
123
+ warnings << 'Transport host not configured (RabbitMQ will use default localhost)' if transport_host.nil? || transport_host.to_s.empty?
123
124
 
124
125
  # Check data config
125
126
  data = Legion::Settings[:data] || {}
@@ -31,6 +31,7 @@ module Legion
31
31
 
32
32
  dir = resolve_config_dir
33
33
  Legion::Settings.load(config_dir: dir)
34
+ Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
34
35
  @settings_ready = true
35
36
  end
36
37
 
@@ -42,7 +42,12 @@ module Legion
42
42
  desc 'diagnose', 'Check environment health and suggest fixes'
43
43
  method_option :fix, type: :boolean, default: false, desc: 'Auto-fix issues where possible'
44
44
  def diagnose
45
- out = formatter
45
+ out = formatter
46
+ begin
47
+ Connection.ensure_settings
48
+ rescue StandardError => e
49
+ Legion::Logging.debug("Doctor#diagnose settings load failed: #{e.message}") if defined?(Legion::Logging)
50
+ end
46
51
  results = run_all_checks
47
52
 
48
53
  if options[:json]
@@ -54,6 +59,8 @@ module Legion
54
59
  auto_fix(results) if options[:fix]
55
60
 
56
61
  exit(1) if results.any?(&:fail?)
62
+ ensure
63
+ Connection.shutdown
57
64
  end
58
65
 
59
66
  default_task :diagnose
@@ -125,6 +125,30 @@ module Legion
125
125
  out.success("Created #{message_path}")
126
126
  end
127
127
 
128
+ desc 'absorber NAME', 'Add an absorber to the current LEX'
129
+ option :url_pattern, type: :string, default: 'example.com/path/*', desc: 'URL pattern to match'
130
+ def absorber(name)
131
+ out = formatter
132
+ lex = detect_lex(out)
133
+
134
+ snake = name.downcase.gsub(/[^a-z0-9]/, '_')
135
+ class_name = snake.split('_').map(&:capitalize).join
136
+ lex_class = lex.split('_').map(&:capitalize).join
137
+ url_pat = options[:url_pattern]
138
+
139
+ absorber_path = "lib/legion/extensions/#{lex}/absorbers/#{snake}.rb"
140
+ spec_path = "spec/absorbers/#{snake}_spec.rb"
141
+
142
+ ensure_dir(File.dirname(absorber_path))
143
+ ensure_dir(File.dirname(spec_path))
144
+
145
+ File.write(absorber_path, absorber_template(lex_class, class_name, url_pat))
146
+ File.write(spec_path, absorber_spec_template(lex_class, class_name, url_pat))
147
+
148
+ out.success("Created #{absorber_path}")
149
+ out.success("Created #{spec_path}")
150
+ end
151
+
128
152
  desc 'tool NAME', 'Add a chat tool to the current LEX'
129
153
  def tool(name)
130
154
  out = formatter
@@ -360,6 +384,58 @@ module Legion
360
384
  end
361
385
  RUBY
362
386
  end
387
+
388
+ def absorber_template(lex_class, class_name, url_pat)
389
+ escaped_pat = url_pat.inspect
390
+ <<~RUBY
391
+ # frozen_string_literal: true
392
+
393
+ module Legion
394
+ module Extensions
395
+ module #{lex_class}
396
+ module Absorbers
397
+ class #{class_name} < Legion::Extensions::Absorbers::Base
398
+ pattern :url, #{escaped_pat}
399
+ description 'TODO: describe what this absorber handles'
400
+
401
+ def handle(url: nil, content: nil, metadata: {}, context: {})
402
+ report_progress(message: 'starting absorption')
403
+
404
+ # TODO: implement content acquisition and processing
405
+ # absorb_to_knowledge(content: text, tags: ['tag'])
406
+
407
+ report_progress(message: 'done', percent: 100)
408
+ { success: true }
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
414
+ end
415
+ RUBY
416
+ end
417
+
418
+ def absorber_spec_template(lex_class, class_name, url_pat)
419
+ test_url = url_pat.gsub('*', 'test')
420
+ <<~RUBY
421
+ # frozen_string_literal: true
422
+
423
+ RSpec.describe Legion::Extensions::#{lex_class}::Absorbers::#{class_name} do
424
+ describe '.patterns' do
425
+ it 'has registered patterns' do
426
+ expect(described_class.patterns).not_to be_empty
427
+ end
428
+ end
429
+
430
+ describe '#handle' do
431
+ it 'returns success' do
432
+ result = described_class.new.handle(url: 'https://#{test_url}')
433
+ expect(result[:success]).to be true
434
+ end
435
+ end
436
+ end
437
+ RUBY
438
+ end
363
439
  end
364
440
  end
365
441
  end
@@ -136,12 +136,26 @@ module Legion
136
136
  llm_kwargs[:model] = options[:model] if options[:model]
137
137
  llm_kwargs[:provider] = options[:provider].to_sym if options[:provider]
138
138
 
139
- Legion::LLM.chat(messages: messages, caller: { source: 'cli', command: 'image' }, **llm_kwargs)
139
+ chat = Legion::LLM.chat(**llm_kwargs)
140
+ user_msg = messages.first
141
+ response = chat.ask(user_msg[:content])
142
+ { content: response.content, usage: extract_usage(response) }
140
143
  rescue StandardError => e
141
144
  out.error("LLM call failed: #{e.message}")
142
145
  raise SystemExit, 1
143
146
  end
144
147
 
148
+ def extract_usage(response)
149
+ return {} unless response.respond_to?(:usage) && response.usage
150
+
151
+ {
152
+ input_tokens: response.usage.input_tokens,
153
+ output_tokens: response.usage.output_tokens
154
+ }
155
+ rescue StandardError
156
+ {}
157
+ end
158
+
145
159
  def render_response(out, response, meta)
146
160
  content = response[:content].to_s
147
161
  usage = response[:usage] || {}
@@ -84,6 +84,7 @@ module Legion
84
84
  Connection.config_dir = options[:config_dir] if options[:config_dir]
85
85
  Connection.log_level = options[:verbose] ? 'debug' : 'error'
86
86
  Connection.ensure_settings
87
+ Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
87
88
  require 'legion/llm'
88
89
  Legion::Settings.merge_settings(:llm, Legion::LLM::Settings.default)
89
90
  end
@@ -121,15 +122,24 @@ module Legion
121
122
  def collect_providers
122
123
  providers_cfg = llm_settings[:providers] || {}
123
124
  providers_cfg.map do |name, cfg|
125
+ enabled = cfg[:enabled] == true
124
126
  {
125
127
  name: name,
126
- enabled: cfg[:enabled] == true,
128
+ enabled: enabled,
129
+ deferred: !enabled && unresolved_credentials?(cfg),
127
130
  default_model: cfg[:default_model],
128
131
  reachable: check_reachable(name, cfg)
129
132
  }
130
133
  end
131
134
  end
132
135
 
136
+ def unresolved_credentials?(cfg)
137
+ %i[api_key secret_key bearer_token password].any? do |key|
138
+ val = cfg[key].to_s
139
+ val.start_with?('vault://', 'lease://', 'env://')
140
+ end
141
+ end
142
+
133
143
  def check_reachable(name, cfg)
134
144
  case name
135
145
  when :ollama
@@ -293,11 +303,19 @@ module Legion
293
303
  when false then 'enabled, unreachable'
294
304
  else 'enabled'
295
305
  end
306
+ elsif p[:deferred]
307
+ 'deferred (credentials pending Vault)'
296
308
  else
297
309
  'disabled'
298
310
  end
299
311
 
300
- color = p[:enabled] ? :green : :muted
312
+ color = if p[:enabled]
313
+ :green
314
+ elsif p[:deferred]
315
+ :yellow
316
+ else
317
+ :muted
318
+ end
301
319
  name_str = p[:name].to_s.ljust(12)
302
320
  model_str = p[:default_model] ? " (#{p[:default_model]})" : ''
303
321
  puts " #{out.colorize(name_str, :label)}#{out.colorize(status, color)}#{model_str}"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'thor'
4
4
  require 'legion/cli/output'
5
+ require 'legion/cli/connection'
5
6
 
6
7
  module Legion
7
8
  module CLI
@@ -12,12 +13,16 @@ module Legion
12
13
  true
13
14
  end
14
15
 
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'
16
+ class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
17
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
18
+ class_option :verbose, type: :boolean, default: false, aliases: ['-V'], desc: 'Verbose logging'
19
+ class_option :config_dir, type: :string, desc: 'Config directory path'
17
20
 
18
21
  desc 'search QUERY', 'Search traces with natural language'
19
22
  option :limit, type: :numeric, default: 50, desc: 'Max results to return'
20
23
  def search(*query_parts)
24
+ return unless setup_connection
25
+
21
26
  require 'legion/trace_search'
22
27
  query = query_parts.join(' ')
23
28
  out = formatter
@@ -38,10 +43,14 @@ module Legion
38
43
  end
39
44
 
40
45
  display_results(out, result)
46
+ ensure
47
+ Legion::CLI::Connection.shutdown
41
48
  end
42
49
 
43
50
  desc 'summarize QUERY', 'Show aggregate statistics for matching traces'
44
51
  def summarize(*query_parts)
52
+ return unless setup_connection
53
+
45
54
  require 'legion/trace_search'
46
55
  query = query_parts.join(' ')
47
56
  out = formatter
@@ -62,6 +71,8 @@ module Legion
62
71
  end
63
72
 
64
73
  display_summary(out, result)
74
+ ensure
75
+ Legion::CLI::Connection.shutdown
65
76
  end
66
77
 
67
78
  default_task :search
@@ -71,6 +82,17 @@ module Legion
71
82
  @formatter ||= Output::Formatter.new(json: options[:json], color: !options[:no_color])
72
83
  end
73
84
 
85
+ def setup_connection
86
+ Legion::CLI::Connection.config_dir = options[:config_dir] if options[:config_dir]
87
+ Legion::CLI::Connection.log_level = options[:verbose] ? 'debug' : 'error'
88
+ Legion::CLI::Connection.ensure_llm
89
+ Legion::CLI::Connection.ensure_data
90
+ true
91
+ rescue CLI::Error => e
92
+ formatter.error("Setup failed: #{e.message}")
93
+ false
94
+ end
95
+
74
96
  private
75
97
 
76
98
  def display_results(out, result)
data/lib/legion/cli.rb CHANGED
@@ -60,7 +60,8 @@ module Legion
60
60
  autoload :Interactive, 'legion/cli/interactive'
61
61
  autoload :Docs, 'legion/cli/docs_command'
62
62
  autoload :Failover, 'legion/cli/failover_command'
63
- autoload :Apollo, 'legion/cli/apollo_command'
63
+ autoload :AbsorbCommand, 'legion/cli/absorb_command'
64
+ autoload :Apollo, 'legion/cli/apollo_command'
64
65
  autoload :TraceCommand, 'legion/cli/trace_command'
65
66
  autoload :Features, 'legion/cli/features_command'
66
67
  autoload :Debug, 'legion/cli/debug_command'
@@ -276,6 +277,9 @@ module Legion
276
277
  desc 'dev SUBCOMMAND', 'Generators, docs, marketplace, and shell completion'
277
278
  subcommand 'dev', Legion::CLI::Groups::Dev
278
279
 
280
+ desc 'absorb SUBCOMMAND', 'Absorb content from external sources'
281
+ subcommand 'absorb', AbsorbCommand
282
+
279
283
  desc 'broker SUBCOMMAND', 'RabbitMQ broker management (stats, cleanup)'
280
284
  subcommand 'broker', Legion::CLI::Broker
281
285
 
@@ -6,13 +6,69 @@ require 'legion/compliance/phi_erasure'
6
6
 
7
7
  module Legion
8
8
  module Compliance
9
+ DEFAULTS = {
10
+ enabled: true,
11
+ classification_level: 'confidential',
12
+ phi_enabled: true,
13
+ pci_enabled: true,
14
+ pii_enabled: true,
15
+ fedramp_enabled: true,
16
+ log_redaction: true,
17
+ cache_phi_max_ttl: 3600
18
+ }.freeze
19
+
9
20
  class << self
21
+ def setup
22
+ return unless defined?(Legion::Settings)
23
+
24
+ Legion::Settings.merge_settings(:compliance, DEFAULTS)
25
+ Legion::Logging.info('[Compliance] max-classification profile active') if defined?(Legion::Logging)
26
+ end
27
+
28
+ def enabled?
29
+ setting(:enabled) == true
30
+ end
31
+
10
32
  def phi_enabled?
11
- return false unless defined?(Legion::Settings)
33
+ setting(:phi_enabled) == true
34
+ end
35
+
36
+ def pci_enabled?
37
+ setting(:pci_enabled) == true
38
+ end
39
+
40
+ def pii_enabled?
41
+ setting(:pii_enabled) == true
42
+ end
43
+
44
+ def fedramp_enabled?
45
+ setting(:fedramp_enabled) == true
46
+ end
47
+
48
+ def classification_level
49
+ setting(:classification_level) || 'confidential'
50
+ end
51
+
52
+ def profile
53
+ {
54
+ classification_level: classification_level,
55
+ phi: phi_enabled?,
56
+ pci: pci_enabled?,
57
+ pii: pii_enabled?,
58
+ fedramp: fedramp_enabled?,
59
+ log_redaction: setting(:log_redaction) == true,
60
+ cache_phi_max_ttl: setting(:cache_phi_max_ttl) || 3600
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def setting(key)
67
+ return nil unless defined?(Legion::Settings)
12
68
 
13
- Legion::Settings[:compliance][:phi_enabled] == true
69
+ Legion::Settings.dig(:compliance, key)
14
70
  rescue StandardError
15
- false
71
+ nil
16
72
  end
17
73
  end
18
74
  end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Absorbers
6
+ class Base
7
+ attr_accessor :job_id, :runners
8
+
9
+ class << self
10
+ def pattern(type, value, priority: 100)
11
+ @patterns ||= []
12
+ @patterns << { type: type, value: value, priority: priority }
13
+ end
14
+
15
+ def patterns
16
+ @patterns || []
17
+ end
18
+
19
+ def description(text = nil)
20
+ text ? @description = text : @description
21
+ end
22
+ end
23
+
24
+ def handle(url: nil, content: nil, metadata: {}, context: {})
25
+ raise NotImplementedError, "#{self.class.name} must implement #handle"
26
+ end
27
+
28
+ def absorb_to_knowledge(content:, tags: [], scope: :global, **opts)
29
+ return fallback_absorb(:chunker, content, tags, scope, opts) unless chunker_available?
30
+ return fallback_absorb(:apollo, content, tags, scope, opts) unless apollo_available?
31
+
32
+ sections = [{ heading: opts.delete(:heading) || 'absorbed',
33
+ content: content,
34
+ section_path: opts.delete(:section_path) || 'absorbed',
35
+ source_file: opts.delete(:source_file) || 'absorber' }]
36
+ chunks = Legion::Extensions::Knowledge::Helpers::Chunker.chunk(sections: sections)
37
+ embeddings = fetch_embeddings(chunks)
38
+ ingest_chunks(chunks, embeddings, tags, scope, opts)
39
+ end
40
+
41
+ def absorb_raw(content:, tags: [], scope: :global, **)
42
+ if apollo_available?
43
+ Legion::Apollo.ingest(content: content, tags: Array(tags), scope: scope, **)
44
+ else
45
+ Legion::Logging.warn('absorb_raw: Apollo not available') if defined?(Legion::Logging)
46
+ { success: false, error: :apollo_not_available }
47
+ end
48
+ end
49
+
50
+ def translate(source, type: :auto)
51
+ raise 'legion-data is required for translate — add it to your Gemfile' unless defined?(Legion::Data::Extract)
52
+
53
+ Legion::Data::Extract.extract(source, type: type)
54
+ end
55
+
56
+ def report_progress(message:, percent: nil)
57
+ return unless job_id
58
+ return unless defined?(Legion::Logging)
59
+
60
+ Legion::Logging.info("absorb[#{job_id}] #{"#{percent}% " if percent}#{message}")
61
+ end
62
+
63
+ private
64
+
65
+ def chunker_available?
66
+ defined?(Legion::Extensions::Knowledge::Helpers::Chunker)
67
+ end
68
+
69
+ def apollo_available?
70
+ defined?(Legion::Apollo) &&
71
+ Legion::Apollo.respond_to?(:ingest) &&
72
+ (!Legion::Apollo.respond_to?(:started?) || Legion::Apollo.started?)
73
+ end
74
+
75
+ def fallback_absorb(reason, content, tags, scope, opts)
76
+ if defined?(Legion::Logging)
77
+ label = reason == :chunker ? 'lex-knowledge not available' : 'Apollo not available'
78
+ Legion::Logging.warn("absorb_to_knowledge: #{label}, falling back to absorb_raw")
79
+ end
80
+ absorb_raw(content: content, tags: tags, scope: scope, **opts)
81
+ end
82
+
83
+ def fetch_embeddings(chunks)
84
+ return [] unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:embed_batch)
85
+
86
+ Legion::LLM.embed_batch(chunks.map { |c| c[:content] })
87
+ rescue StandardError
88
+ []
89
+ end
90
+
91
+ def ingest_chunks(chunks, embeddings, tags, scope, opts)
92
+ chunks.each_with_index do |chunk, idx|
93
+ vector = embeddings.is_a?(Array) ? embeddings.dig(idx, :vector) : nil
94
+ payload = build_chunk_payload(chunk, tags, opts)
95
+ payload[:embedding] = vector if vector
96
+ Legion::Apollo.ingest(content: payload[:content], tags: payload[:tags],
97
+ scope: scope, **payload.except(:content, :tags))
98
+ end
99
+ end
100
+
101
+ def build_chunk_payload(chunk, tags, opts)
102
+ {
103
+ content: chunk[:content],
104
+ content_type: opts[:content_type] || 'absorbed_chunk',
105
+ content_hash: chunk[:content_hash],
106
+ tags: (Array(tags) + [chunk[:heading], 'absorbed']).compact.uniq,
107
+ metadata: {
108
+ source_file: chunk[:source_file],
109
+ heading: chunk[:heading],
110
+ section_path: chunk[:section_path],
111
+ chunk_index: chunk[:chunk_index],
112
+ token_count: chunk[:token_count]
113
+ }.merge(opts.fetch(:metadata, {}))
114
+ }
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Absorbers
6
+ module Matchers
7
+ class Base
8
+ @registry = {}
9
+
10
+ class << self
11
+ attr_reader :registry
12
+
13
+ def inherited(subclass)
14
+ super
15
+ TracePoint.new(:end) do |tp|
16
+ if tp.self == subclass
17
+ register(subclass) if subclass.respond_to?(:type) && subclass.type
18
+ tp.disable
19
+ end
20
+ end.enable
21
+ end
22
+
23
+ def register(matcher_class)
24
+ @registry[matcher_class.type] = matcher_class
25
+ end
26
+
27
+ def for_type(type)
28
+ @registry[type&.to_sym]
29
+ end
30
+
31
+ def type = nil
32
+
33
+ def match?(_pattern, _input)
34
+ raise NotImplementedError, "#{name} must implement .match?"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Absorbers
8
+ module Matchers
9
+ class Url < Base
10
+ def self.type = :url
11
+
12
+ def self.match?(pattern, input)
13
+ uri = parse_uri(input)
14
+ return false unless uri
15
+
16
+ host_pattern, path_pattern = split_pattern(pattern)
17
+ return false unless host_matches?(host_pattern, uri.host)
18
+
19
+ path_matches?(path_pattern || '**', uri.path)
20
+ end
21
+
22
+ class << self
23
+ private
24
+
25
+ def parse_uri(input)
26
+ str = input.to_s.strip
27
+ str = "https://#{str}" unless str.match?(%r{\A\w+://})
28
+ uri = URI.parse(str)
29
+ return nil unless uri.is_a?(URI::HTTP) && uri.host
30
+
31
+ uri
32
+ rescue URI::InvalidURIError
33
+ nil
34
+ end
35
+
36
+ def split_pattern(pattern)
37
+ clean = pattern.sub(%r{\A\w+://}, '')
38
+ parts = clean.split('/', 2)
39
+ [parts[0], parts[1]]
40
+ end
41
+
42
+ def host_matches?(pattern, host)
43
+ return false unless host
44
+
45
+ regex = Regexp.new(
46
+ "\\A#{Regexp.escape(pattern).gsub('\\*', '[^.]+')}\\z",
47
+ Regexp::IGNORECASE
48
+ )
49
+ regex.match?(host)
50
+ end
51
+
52
+ def path_matches?(pattern, path)
53
+ path = path.to_s.sub(%r{\A/}, '')
54
+ escaped = Regexp.escape(pattern)
55
+ .gsub('\\*\\*', '__.DOUBLE_STAR__.')
56
+ .gsub('\\*', '[^/]*')
57
+ .gsub('__.DOUBLE_STAR__.', '.*')
58
+ Regexp.new("\\A#{escaped}\\z").match?(path)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Absorbers
6
+ module PatternMatcher
7
+ @registrations = []
8
+ @mutex = Mutex.new
9
+
10
+ module_function
11
+
12
+ def register(absorber_class)
13
+ @mutex.synchronize do
14
+ absorber_class.patterns.each do |pat|
15
+ @registrations << {
16
+ type: pat[:type],
17
+ value: pat[:value],
18
+ priority: pat[:priority],
19
+ absorber_class: absorber_class,
20
+ description: absorber_class.description
21
+ }
22
+ end
23
+ end
24
+ end
25
+
26
+ def resolve(input)
27
+ matches = @mutex.synchronize { @registrations.dup }.select do |reg|
28
+ matcher = Matchers::Base.for_type(reg[:type])
29
+ next false unless matcher
30
+
31
+ matcher.match?(reg[:value], input)
32
+ end
33
+ return nil if matches.empty?
34
+
35
+ matches.min_by { |m| [m[:priority], -m[:value].gsub('*', '').length] }&.dig(:absorber_class)
36
+ end
37
+
38
+ def list
39
+ @mutex.synchronize { @registrations.dup }
40
+ end
41
+
42
+ def registrations
43
+ @mutex.synchronize { @registrations.dup }
44
+ end
45
+
46
+ def reset!
47
+ @mutex.synchronize { @registrations.clear }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'absorbers/matchers/base'
4
+ require_relative 'absorbers/matchers/url'
5
+ require_relative 'absorbers/base'
6
+ require_relative 'absorbers/pattern_matcher'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Absorbers
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Actors
8
+ module AbsorberDispatch
9
+ module_function
10
+
11
+ def dispatch(input:, job_id: nil, context: {})
12
+ job_id ||= SecureRandom.hex(8)
13
+ absorber_class = Absorbers::PatternMatcher.resolve(input)
14
+
15
+ unless absorber_class
16
+ publish_event("absorb.failed.#{job_id}", job_id: job_id, error: 'no handler found for input')
17
+ return { success: false, error: 'no handler found for input', job_id: job_id }
18
+ end
19
+
20
+ absorber = absorber_class.new
21
+ absorber.job_id = job_id
22
+ result = absorber.handle(url: input, content: context[:content],
23
+ metadata: context[:metadata] || {}, context: context)
24
+ publish_event("absorb.complete.#{job_id}", job_id: job_id, absorber: absorber_class.name,
25
+ result: result)
26
+ { success: true, job_id: job_id, absorber: absorber_class.name, result: result }
27
+ rescue StandardError => e
28
+ Legion::Logging.error("AbsorberDispatch failed: #{e.message}") if defined?(Legion::Logging)
29
+ publish_event("absorb.failed.#{job_id}", job_id: job_id, error: e.message)
30
+ { success: false, job_id: job_id, error: e.message }
31
+ end
32
+
33
+ def publish_event(routing_key, **payload)
34
+ return unless defined?(Legion::Transport)
35
+
36
+ session = Legion::Transport.respond_to?(:session) ? Legion::Transport.session : nil
37
+ if session.respond_to?(:open?)
38
+ return unless session.open?
39
+ elsif session.nil?
40
+ return
41
+ end
42
+
43
+ message_class =
44
+ if defined?(Legion::Transport::Messages::Dynamic)
45
+ Legion::Transport::Messages::Dynamic
46
+ elsif defined?(Legion::Transport::Message)
47
+ Legion::Transport::Message
48
+ end
49
+ return unless message_class
50
+
51
+ message_class.new(routing_key: routing_key, **payload).publish
52
+ rescue StandardError => e
53
+ Legion::Logging.warn("AbsorberDispatch publish failed: #{e.message}") if defined?(Legion::Logging)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Builder
8
+ module Absorbers
9
+ include Legion::Extensions::Builder::Base
10
+
11
+ def build_absorbers
12
+ @absorbers = {}
13
+ absorber_files = find_files('absorbers')
14
+ return if absorber_files.empty?
15
+
16
+ require_files(absorber_files)
17
+
18
+ absorber_files.each do |file|
19
+ snake_name = file.split('/').last.sub('.rb', '')
20
+ class_name = snake_name.split('_').collect(&:capitalize).join
21
+ absorber_class = "#{lex_class}::Absorbers::#{class_name}"
22
+
23
+ next unless Kernel.const_defined?(absorber_class)
24
+
25
+ klass = Kernel.const_get(absorber_class)
26
+ next unless klass < Legion::Extensions::Absorbers::Base
27
+
28
+ @absorbers[snake_name.to_sym] = {
29
+ extension: lex_name,
30
+ extension_class: lex_class,
31
+ absorber_name: snake_name,
32
+ absorber_class: absorber_class,
33
+ absorber_module: klass,
34
+ patterns: klass.patterns,
35
+ description: klass.description
36
+ }
37
+
38
+ Legion::Extensions::Absorbers::PatternMatcher.register(klass)
39
+ end
40
+ rescue StandardError => e
41
+ Legion::Logging.error("Failed to build absorbers: #{e.message}") if defined?(Legion::Logging)
42
+ end
43
+
44
+ def absorbers
45
+ @absorbers || {}
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -6,6 +6,22 @@ module Legion
6
6
  :name, :extension, :runner, :function,
7
7
  :description, :parameters, :tags, :loaded_at
8
8
  ) do
9
+ def self.from_absorber(extension:, absorber:, patterns: [], description: nil)
10
+ absorber_name = absorber.name&.split('::')&.last || absorber.object_id.to_s
11
+ snake = absorber_name.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
12
+ canonical = "#{extension}:absorber:#{snake}"
13
+ new(
14
+ name: canonical,
15
+ extension: extension,
16
+ runner: 'Absorber',
17
+ function: absorber_name,
18
+ description: description,
19
+ parameters: { input: { type: :string, required: true } },
20
+ tags: ['absorber'] + patterns.map { |p| "pattern:#{p[:type]}:#{p[:value]}" },
21
+ loaded_at: Time.now
22
+ )
23
+ end
24
+
9
25
  def self.from_runner(extension:, runner:, function:, **opts)
10
26
  canonical = "#{extension}:#{runner.to_s.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase}:#{function}"
11
27
  new(
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'absorbers'
4
+ require_relative 'builders/absorbers'
3
5
  require_relative 'builders/actors'
4
6
  require_relative 'builders/helpers'
5
7
  require_relative 'builders/hooks'
@@ -49,6 +51,7 @@ module Legion
49
51
  include Legion::Extensions::Helpers::Lex
50
52
  include Legion::Extensions::Helpers::Knowledge if defined?(Legion::Extensions::Helpers::Knowledge)
51
53
 
54
+ include Legion::Extensions::Builder::Absorbers
52
55
  include Legion::Extensions::Builder::Runners
53
56
  include Legion::Extensions::Builder::Helpers
54
57
  include Legion::Extensions::Builder::Actors
@@ -73,6 +76,7 @@ module Legion
73
76
  end
74
77
  build_helpers
75
78
  build_runners
79
+ build_absorbers
76
80
  build_actors
77
81
  build_hooks
78
82
  build_routes
@@ -229,6 +229,7 @@ module Legion
229
229
  Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners).publish
230
230
 
231
231
  register_capabilities(entry[:gem_name], extension.runners) if extension.respond_to?(:runners)
232
+ register_absorber_capabilities(entry[:gem_name], extension.absorbers) if extension.respond_to?(:absorbers)
232
233
 
233
234
  if extension.respond_to?(:meta_actors) && extension.meta_actors.is_a?(Hash)
234
235
  extension.meta_actors.each_value do |actor|
@@ -502,6 +503,25 @@ module Legion
502
503
  Extensions::Catalog::Registry.unregister_extension(gem_name)
503
504
  end
504
505
 
506
+ def register_absorber_capabilities(gem_name, absorbers)
507
+ absorbers.each_value do |absorber_meta|
508
+ cap = Extensions::Capability.from_absorber(
509
+ extension: gem_name,
510
+ absorber: absorber_meta[:absorber_module],
511
+ patterns: absorber_meta[:patterns],
512
+ description: absorber_meta[:description]
513
+ )
514
+ Extensions::Catalog::Registry.register(cap)
515
+ rescue StandardError => e
516
+ if defined?(Legion::Logging)
517
+ Legion::Logging.warn(
518
+ "Absorber catalog registration error for #{gem_name} " \
519
+ "(#{absorber_meta[:absorber_module]}): #{e.message}"
520
+ )
521
+ end
522
+ end
523
+ end
524
+
505
525
  def register_capabilities(gem_name, runners)
506
526
  runners.each_value do |runner_meta|
507
527
  runner_name = runner_meta[:runner_name]
@@ -31,6 +31,7 @@ module Legion
31
31
  Legion::Logging.debug('Starting Legion::Service')
32
32
  setup_settings
33
33
  apply_cli_overrides(http_port: http_port)
34
+ setup_compliance
34
35
  setup_local_mode
35
36
  reconfigure_logging(log_level)
36
37
  Legion::Logging.info("node name: #{Legion::Settings[:client][:name]}")
@@ -225,6 +226,15 @@ module Legion
225
226
  self.class.log_privacy_mode_status
226
227
  end
227
228
 
229
+ def setup_compliance
230
+ require 'legion/compliance'
231
+ Legion::Compliance.setup
232
+ rescue LoadError => e
233
+ Legion::Logging.debug "Compliance module not available: #{e.message}"
234
+ rescue StandardError => e
235
+ Legion::Logging.warn "Compliance setup failed: #{e.message}"
236
+ end
237
+
228
238
  def apply_cli_overrides(http_port: nil)
229
239
  return unless http_port
230
240
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.6.13'
4
+ VERSION = '1.6.18'
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.6.13
4
+ version: 1.6.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -509,6 +509,7 @@ files:
509
509
  - lib/legion/chat/notification_queue.rb
510
510
  - lib/legion/chat/skills.rb
511
511
  - lib/legion/cli.rb
512
+ - lib/legion/cli/absorb_command.rb
512
513
  - lib/legion/cli/acp_command.rb
513
514
  - lib/legion/cli/apollo_command.rb
514
515
  - lib/legion/cli/audit_command.rb
@@ -740,6 +741,12 @@ files:
740
741
  - lib/legion/docs/site_generator.rb
741
742
  - lib/legion/events.rb
742
743
  - lib/legion/extensions.rb
744
+ - lib/legion/extensions/absorbers.rb
745
+ - lib/legion/extensions/absorbers/base.rb
746
+ - lib/legion/extensions/absorbers/matchers/base.rb
747
+ - lib/legion/extensions/absorbers/matchers/url.rb
748
+ - lib/legion/extensions/absorbers/pattern_matcher.rb
749
+ - lib/legion/extensions/actors/absorber_dispatch.rb
743
750
  - lib/legion/extensions/actors/base.rb
744
751
  - lib/legion/extensions/actors/defaults.rb
745
752
  - lib/legion/extensions/actors/every.rb
@@ -750,6 +757,7 @@ files:
750
757
  - lib/legion/extensions/actors/poll.rb
751
758
  - lib/legion/extensions/actors/singleton.rb
752
759
  - lib/legion/extensions/actors/subscription.rb
760
+ - lib/legion/extensions/builders/absorbers.rb
753
761
  - lib/legion/extensions/builders/actors.rb
754
762
  - lib/legion/extensions/builders/base.rb
755
763
  - lib/legion/extensions/builders/helpers.rb