legionio 1.5.8 → 1.5.9

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: dd1e62d0c34e503458b9648de476a15dd5c03a163bb506db8a4723da486aef24
4
- data.tar.gz: 43147d5ba26541cf62f3d5255ce973480fccb1362a5d8daf224ff16b82b6e55f
3
+ metadata.gz: e7ed91b745c34247336b6d3c1d040b6773d21d531445ea7b42149cdbf4bfc170
4
+ data.tar.gz: 1eedc2e5b6011c0ed4a9b35d65e9be14bb474b46851720a30667e1167060633a
5
5
  SHA512:
6
- metadata.gz: af364276c4d9589d8ee414a2e563640be7c49c472a0ef566a035b502198d7396acd055f42f576756a9bbd0beda57f09e0bb098bcb15b387da8dbf31baa728120
7
- data.tar.gz: 44671d558a7e20e9dbda648e0ad71efd0c9db546d37507c0e21d3367a9e4c896b79476f8158597e36eb310c46415d1084dea9cfbe389405b7940a74c10609c63
6
+ metadata.gz: d89facba54ed5b92539bb40615837412b6e091283ce13fcfa6a4ac460fd204a80e103327c873afedaf62f81aa45a6a4390f97c6a945841b8fef9cc25c6a7a699
7
+ data.tar.gz: c298fc10efbc5a2d0a3ce69ccdc2c36976fc2f86700f3d7589ac060e97a3517cc47ea22807c9755da25390d8b2d22080abe523feba9e4b3aa6406a0ddd83ce0d
data/.rubocop.yml CHANGED
@@ -52,6 +52,7 @@ Metrics/BlockLength:
52
52
  - 'lib/legion/cli/failover_command.rb'
53
53
  - 'lib/legion/cli/setup_command.rb'
54
54
  - 'lib/legion/cli/trace_command.rb'
55
+ - 'lib/legion/cli/features_command.rb'
55
56
 
56
57
  Metrics/AbcSize:
57
58
  Max: 60
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.5.9] - 2026-03-25
4
+
5
+ ### Fixed
6
+ - `Subscription#activate` nil guard — skip activate when `@consumer` is nil (prepare failed silently)
7
+ - `Extensions#shutdown` tracks real actor instances in `@running_instances`, cancels them with deadline-based drain
8
+ - `Extensions::Helpers::Base` runner_class derivation improvements for self-contained actors
9
+
10
+ ### Changed
11
+ - Bumped gemspec dependencies: legion-cache >= 1.3.16, legion-settings >= 1.3.19, legion-transport >= 1.4.0, legion-mcp >= 0.5.1
12
+
3
13
  ## [1.5.8] - 2026-03-24
4
14
 
5
15
  ### Added
data/legionio.gemspec CHANGED
@@ -52,13 +52,13 @@ Gem::Specification.new do |spec|
52
52
  spec.add_dependency 'thor', '>= 1.3'
53
53
  spec.add_dependency 'tty-spinner', '~> 0.9'
54
54
 
55
- spec.add_dependency 'legion-cache', '>= 1.3.11'
56
- spec.add_dependency 'legion-crypt', '>= 1.4.9'
55
+ spec.add_dependency 'legion-cache', '>= 1.3.16'
56
+ spec.add_dependency 'legion-crypt', '>= 1.4.12'
57
57
  spec.add_dependency 'legion-data', '>= 1.5.0'
58
58
  spec.add_dependency 'legion-json', '>= 1.2.1'
59
59
  spec.add_dependency 'legion-logging', '>= 1.3.2'
60
- spec.add_dependency 'legion-settings', '>= 1.3.14'
61
- spec.add_dependency 'legion-transport', '>= 1.3.11'
60
+ spec.add_dependency 'legion-settings', '>= 1.3.19'
61
+ spec.add_dependency 'legion-transport', '>= 1.4.0'
62
62
 
63
63
  spec.add_dependency 'legion-tty', '>= 0.4.34'
64
64
  spec.add_dependency 'lex-node'
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+ require 'thor'
5
+ require 'rbconfig'
6
+ require 'legion/cli/output'
7
+
8
+ module Legion
9
+ module CLI
10
+ class Features < Thor
11
+ namespace 'features'
12
+
13
+ def self.exit_on_failure?
14
+ true
15
+ end
16
+
17
+ class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
18
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
19
+
20
+ BUNDLES = {
21
+ tasking: {
22
+ label: 'Tasking Engine',
23
+ description: 'Task scheduling, chaining, conditioning, and metering',
24
+ gems: %w[lex-tasker lex-scheduler lex-lex lex-conditioner lex-transformer lex-health lex-metering]
25
+ },
26
+ cognitive: {
27
+ label: 'Cognitive / Agentic',
28
+ description: 'Full GAIA cognitive stack (13 agentic domains + tick + mesh + apollo)',
29
+ gems: %w[legion-gaia]
30
+ },
31
+ ai: {
32
+ label: 'AI / LLM',
33
+ description: 'LLM routing, provider integration, and MCP tools',
34
+ gems: %w[legion-llm legion-mcp]
35
+ },
36
+ observability: {
37
+ label: 'Observability',
38
+ description: 'Telemetry, logging, anomaly detection, and webhooks',
39
+ gems: %w[lex-telemetry lex-log lex-webhook lex-detect]
40
+ },
41
+ governance: {
42
+ label: 'Governance & Security',
43
+ description: 'RBAC, audit trails, FinOps, PII protection, and lifecycle governance',
44
+ gems: %w[lex-governance lex-audit lex-finops lex-privatecore]
45
+ },
46
+ channels: {
47
+ label: 'Chat Channels',
48
+ description: 'Slack, Microsoft Teams, and GitHub chat adapters',
49
+ gems: %w[lex-slack lex-microsoft_teams lex-github]
50
+ },
51
+ devtools: {
52
+ label: 'Development Tools',
53
+ description: 'Eval gating, datasets, prompt templates, autofix, and mind-growth',
54
+ gems: %w[lex-eval lex-dataset lex-prompt lex-autofix lex-mind-growth]
55
+ },
56
+ swarm: {
57
+ label: 'Swarm / Multi-Agent',
58
+ description: 'Multi-agent orchestration, GitHub swarm pipeline, and ACP adapter',
59
+ gems: %w[lex-swarm lex-swarm-github lex-adapter lex-acp]
60
+ },
61
+ services: {
62
+ label: 'Service Integrations',
63
+ description: 'HTTP, Vault, and Consul service connectors',
64
+ gems: %w[lex-http lex-vault lex-consul]
65
+ }
66
+ }.freeze
67
+
68
+ desc 'install', 'Interactively select and install feature bundles'
69
+ option :dry_run, type: :boolean, default: false, desc: 'Show what would be installed without installing'
70
+ option :all, type: :boolean, default: false, desc: 'Install all feature bundles'
71
+ def install
72
+ out = formatter
73
+ selected = options[:all] ? BUNDLES.keys : prompt_bundle_selection(out)
74
+
75
+ return out.error('No bundles selected') if selected.empty?
76
+
77
+ gems = resolve_gems(selected)
78
+ installed, missing = partition_gems(gems)
79
+
80
+ if missing.empty?
81
+ report_all_present(out, selected, installed)
82
+ elsif options[:dry_run]
83
+ report_dry_run(out, selected, installed, missing)
84
+ else
85
+ execute_install(out, selected, installed, missing)
86
+ end
87
+ end
88
+
89
+ desc 'list', 'Show available feature bundles and their install status'
90
+ def list
91
+ out = formatter
92
+ statuses = bundle_statuses
93
+
94
+ if options[:json]
95
+ out.json(bundles: statuses)
96
+ else
97
+ out.header('Feature Bundles')
98
+ out.spacer
99
+ statuses.each { |s| print_bundle_status(out, s) }
100
+ out.spacer
101
+ installed_count = statuses.count { |s| s[:missing].empty? }
102
+ puts " #{installed_count} of #{statuses.size} bundle(s) fully installed"
103
+ end
104
+ end
105
+
106
+ no_commands do
107
+ def formatter
108
+ @formatter ||= Output::Formatter.new(
109
+ json: options[:json],
110
+ color: !options[:no_color]
111
+ )
112
+ end
113
+
114
+ private
115
+
116
+ def prompt_bundle_selection(out)
117
+ require 'tty-prompt'
118
+ prompt = ::TTY::Prompt.new
119
+ statuses = bundle_statuses
120
+
121
+ choices = statuses.map do |s|
122
+ icon = s[:missing].empty? ? '(installed)' : "(#{s[:missing].size} gem(s) to install)"
123
+ { name: "#{s[:label]} #{icon} - #{s[:description]}", value: s[:name] }
124
+ end
125
+ choices << { name: 'Everything - install all bundles above', value: :everything }
126
+
127
+ out.header('Legion Feature Bundles')
128
+ out.spacer
129
+
130
+ selected = prompt.multi_select('Select bundles to install:', choices, per_page: 12,
131
+ echo: false,
132
+ min: 1)
133
+ return BUNDLES.keys if selected.include?(:everything)
134
+
135
+ selected
136
+ rescue ::TTY::Reader::InputInterrupt, Interrupt
137
+ out.spacer
138
+ puts ' Cancelled.'
139
+ []
140
+ end
141
+
142
+ def resolve_gems(bundle_keys)
143
+ bundle_keys.flat_map { |key| BUNDLES[key][:gems] }.uniq.sort
144
+ end
145
+
146
+ def partition_gems(gem_names)
147
+ installed = []
148
+ missing = []
149
+ gem_names.each do |name|
150
+ Gem::Specification.find_by_name(name)
151
+ installed << name
152
+ rescue Gem::MissingSpecError
153
+ missing << name
154
+ end
155
+ [installed, missing]
156
+ end
157
+
158
+ def gem_version(name)
159
+ Gem::Specification.find_by_name(name).version.to_s
160
+ rescue Gem::MissingSpecError
161
+ nil
162
+ end
163
+
164
+ def bundle_statuses
165
+ BUNDLES.map do |name, bundle|
166
+ installed, missing = partition_gems(bundle[:gems])
167
+ {
168
+ name: name,
169
+ label: bundle[:label],
170
+ description: bundle[:description],
171
+ installed: installed.map { |g| { name: g, version: gem_version(g) } },
172
+ missing: missing
173
+ }
174
+ end
175
+ end
176
+
177
+ def print_bundle_status(out, status)
178
+ icon = if status[:missing].empty?
179
+ out.colorize('installed', :success)
180
+ else
181
+ out.colorize("#{status[:missing].size} missing", :muted)
182
+ end
183
+ puts " #{out.colorize(status[:label].ljust(24), :label)} #{icon}"
184
+ status[:installed].each do |g|
185
+ puts " #{out.colorize(g[:name], :success)} #{g[:version]}"
186
+ end
187
+ status[:missing].each do |g|
188
+ puts " #{out.colorize(g, :muted)} (not installed)"
189
+ end
190
+ end
191
+
192
+ def report_all_present(out, selected, installed)
193
+ labels = selected.map { |k| BUNDLES[k][:label] }.join(', ')
194
+ if options[:json]
195
+ out.json(status: 'already_installed', bundles: selected,
196
+ gems: installed.map { |g| { name: g, version: gem_version(g) } })
197
+ else
198
+ out.success("All gems already installed for: #{labels}")
199
+ installed.each { |g| puts " #{g} #{gem_version(g)}" }
200
+ end
201
+ end
202
+
203
+ def report_dry_run(out, selected, installed, missing)
204
+ labels = selected.map { |k| BUNDLES[k][:label] }.join(', ')
205
+ if options[:json]
206
+ out.json(status: 'dry_run', bundles: selected, to_install: missing,
207
+ already_installed: installed.map { |g| { name: g, version: gem_version(g) } })
208
+ else
209
+ out.header("Feature install dry run: #{labels}")
210
+ out.spacer
211
+ missing.each { |g| puts " #{out.colorize('install', :accent)} #{g}" }
212
+ installed.each { |g| puts " #{out.colorize('skip', :muted)} #{g} #{gem_version(g)} (already installed)" }
213
+ end
214
+ end
215
+
216
+ def execute_install(out, selected, installed, missing)
217
+ labels = selected.map { |k| BUNDLES[k][:label] }.join(', ')
218
+ out.header("Installing: #{labels}") unless options[:json]
219
+ out.spacer unless options[:json]
220
+ puts " #{missing.size} gem(s) to install, #{installed.size} already present" unless options[:json]
221
+ out.spacer unless options[:json]
222
+
223
+ gem_bin = File.join(RbConfig::CONFIG['bindir'], 'gem')
224
+ results = missing.map { |g| install_gem(g, gem_bin, out) }
225
+
226
+ Gem::Specification.reset
227
+ successes, failures = results.partition { |r| r[:status] == 'installed' }
228
+
229
+ if options[:json]
230
+ out.json(bundles: selected, installed: successes, failed: failures,
231
+ already_present: installed.map { |g| { name: g, version: gem_version(g) } })
232
+ else
233
+ out.spacer
234
+ if failures.empty?
235
+ out.success("#{successes.size} gem(s) installed successfully")
236
+ else
237
+ out.error("#{failures.size} gem(s) failed to install")
238
+ failures.each { |f| puts " #{f[:name]}: #{f[:error]}" }
239
+ out.spacer
240
+ out.success("#{successes.size} gem(s) installed") unless successes.empty?
241
+ end
242
+ suggest_next_steps(out, selected)
243
+ end
244
+ end
245
+
246
+ def install_gem(name, gem_bin, out)
247
+ puts " Installing #{name}..." unless options[:json]
248
+ output = `#{gem_bin} install #{name} --no-document 2>&1`
249
+ if $CHILD_STATUS.success?
250
+ out.success(" #{name} installed") unless options[:json]
251
+ { name: name, status: 'installed' }
252
+ else
253
+ out.error(" #{name} failed") unless options[:json]
254
+ { name: name, status: 'failed', error: output.strip.lines.last&.strip }
255
+ end
256
+ end
257
+
258
+ def suggest_next_steps(out, selected)
259
+ out.spacer
260
+ puts ' Next steps:'
261
+ if selected.include?(:cognitive) || selected.include?(:ai)
262
+ puts ' legion start # full daemon with cognitive stack'
263
+ puts ' legion start --lite # single-process, no external services'
264
+ puts ' legion chat # interactive AI conversation'
265
+ end
266
+ puts ' legion features list # verify installed bundles'
267
+ puts ' legion doctor # check environment health'
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
data/lib/legion/cli.rb CHANGED
@@ -61,6 +61,7 @@ module Legion
61
61
  autoload :Failover, 'legion/cli/failover_command'
62
62
  autoload :Apollo, 'legion/cli/apollo_command'
63
63
  autoload :TraceCommand, 'legion/cli/trace_command'
64
+ autoload :Features, 'legion/cli/features_command'
64
65
 
65
66
  class Main < Thor
66
67
  def self.exit_on_failure?
@@ -338,6 +339,9 @@ module Legion
338
339
  desc 'trace SUBCOMMAND', 'Natural language trace search via LLM'
339
340
  subcommand 'trace', Legion::CLI::TraceCommand
340
341
 
342
+ desc 'features SUBCOMMAND', 'Install feature bundles (interactive selector)'
343
+ subcommand 'features', Legion::CLI::Features
344
+
341
345
  desc 'tree', 'Print a tree of all available commands'
342
346
  def tree
343
347
  legion_print_command_tree(self.class, 'legion', '')
@@ -86,6 +86,10 @@ module Legion
86
86
  end
87
87
 
88
88
  def activate
89
+ unless @consumer
90
+ log.warn "[Subscription] skipping activate for #{lex_name}/#{runner_name}: no consumer (prepare failed?)"
91
+ return
92
+ end
89
93
  @queue.subscribe_with(@consumer)
90
94
  log.info "[Subscription] activated: #{lex_name}/#{runner_name} (consumer registered)"
91
95
  end
@@ -96,8 +96,13 @@ module Legion
96
96
 
97
97
  def full_path
98
98
  @full_path ||= begin
99
- gem_name = "lex-#{segments.join('-')}"
100
- gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir
99
+ base_name = segments.join('-')
100
+ gem_name = "lex-#{base_name}"
101
+ gem_dir = begin
102
+ Gem::Specification.find_by_name(gem_name).gem_dir
103
+ rescue Gem::MissingSpecError
104
+ Gem::Specification.find_by_name("lex-#{base_name.tr('_', '-')}").gem_dir
105
+ end
101
106
  require_path = Helpers::Segments.derive_require_path(gem_name)
102
107
  "#{gem_dir}/lib/#{require_path}"
103
108
  end
@@ -106,12 +106,12 @@ module Legion
106
106
 
107
107
  def bind_e_to_q(to:, from: default_exchange, routing_key: nil, **)
108
108
  if from.is_a? String
109
- from = "#{transport_class}::Exchanges::#{from.split('_').collect(&:capitalize).join}" unless from.include?('::')
109
+ from = "#{transport_class}::Exchanges::#{from.tr('.', '_').split('_').collect(&:capitalize).join}" unless from.include?('::')
110
110
  auto_create_exchange(from) unless Object.const_defined? from
111
111
  end
112
112
 
113
113
  if to.is_a? String
114
- to = "#{transport_class}::Queues::#{to.split('_').collect(&:capitalize).join}" unless to.include?('::')
114
+ to = "#{transport_class}::Queues::#{to.tr('.', '_').split('_').collect(&:capitalize).join}" unless to.include?('::')
115
115
  auto_create_queue(to) unless Object.const_defined?(to)
116
116
  end
117
117
 
@@ -21,6 +21,7 @@ module Legion
21
21
  @subscription_tasks = []
22
22
  @local_tasks = []
23
23
  @actors = []
24
+ @running_instances = Concurrent::Array.new
24
25
  @pending_actors = Concurrent::Array.new
25
26
 
26
27
  find_extensions
@@ -31,9 +32,12 @@ module Legion
31
32
 
32
33
  attr_reader :local_tasks
33
34
 
34
- def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
35
+ def shutdown # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
35
36
  return nil if @loaded_extensions.nil?
36
37
 
38
+ deadline = Legion::Settings.dig(:extensions, :shutdown_timeout) || 15
39
+ shutdown_start = Time.now
40
+
37
41
  @loaded_extensions.each { |name| Catalog.transition(name, :stopping) }
38
42
 
39
43
  if @subscription_pool
@@ -42,22 +46,54 @@ module Legion
42
46
  @subscription_pool = nil
43
47
  end
44
48
 
45
- @subscription_tasks.each do |task|
46
- task[:running_class]&.new&.cancel if task[:running_class].is_a?(Class)
49
+ # Cancel all running instances (real objects, not new instances)
50
+ @running_instances&.each do |instance|
51
+ instance.cancel if instance.respond_to?(:cancel)
47
52
  rescue StandardError => e
48
53
  Legion::Logging.debug "Extension shutdown cancel failed: #{e.message}" if defined?(Legion::Logging)
49
54
  end
50
55
 
51
- @loop_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
52
- @once_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
53
- @timer_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
54
- @poll_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
56
+ # Wait for in-flight work to drain, up to deadline
57
+ remaining = deadline - (Time.now - shutdown_start)
58
+ if remaining.positive?
59
+ drain_start = Time.now
60
+ loop do
61
+ elapsed = Time.now - drain_start
62
+ break if elapsed >= remaining
63
+
64
+ still_active = @running_instances&.any? do |inst|
65
+ (inst.respond_to?(:channel) && inst.instance_variable_get(:@queue)&.channel&.open?) ||
66
+ (inst.instance_variable_get(:@timer).respond_to?(:running?) && inst.instance_variable_get(:@timer).running?) ||
67
+ (inst.instance_variable_get(:@loop) == true)
68
+ end
69
+ break unless still_active
70
+
71
+ sleep 0.25
72
+ end
73
+ end
74
+
75
+ # Force-close any channels still open after deadline
76
+ elapsed = Time.now - shutdown_start
77
+ if elapsed >= deadline
78
+ Legion::Logging.warn "Shutdown deadline (#{deadline}s) reached, force-closing remaining actors" if defined?(Legion::Logging)
79
+ @running_instances&.each do |inst|
80
+ queue = inst.instance_variable_get(:@queue)
81
+ queue&.channel&.close if queue&.channel.respond_to?(:close) && queue.channel.open?
82
+ timer = inst.instance_variable_get(:@timer)
83
+ timer&.kill if timer.respond_to?(:kill)
84
+ inst.instance_variable_set(:@loop, false) if inst.instance_variable_defined?(:@loop)
85
+ rescue StandardError => e
86
+ Legion::Logging.debug "Force-close failed: #{e.message}" if defined?(Legion::Logging)
87
+ end
88
+ end
89
+
90
+ @running_instances&.clear
55
91
 
56
92
  @loaded_extensions.each do |name|
57
93
  Catalog.transition(name, :stopped)
58
94
  unregister_capabilities(name)
59
95
  end
60
- Legion::Logging.info 'Successfully shut down all actors'
96
+ Legion::Logging.info "Successfully shut down all actors (#{(Time.now - shutdown_start).round(1)}s)"
61
97
  end
62
98
 
63
99
  def load_extensions
@@ -276,12 +312,16 @@ module Legion
276
312
 
277
313
  if actor_class.ancestors.include? Legion::Extensions::Actors::Every
278
314
  @timer_tasks.push(extension_hash)
315
+ @running_instances << extension_hash[:running_class]
279
316
  elsif actor_class.ancestors.include? Legion::Extensions::Actors::Once
280
317
  @once_tasks.push(extension_hash)
318
+ @running_instances << extension_hash[:running_class]
281
319
  elsif actor_class.ancestors.include? Legion::Extensions::Actors::Loop
282
320
  @loop_tasks.push(extension_hash)
321
+ @running_instances << extension_hash[:running_class]
283
322
  elsif actor_class.ancestors.include? Legion::Extensions::Actors::Poll
284
323
  @poll_tasks.push(extension_hash)
324
+ @running_instances << extension_hash[:running_class]
285
325
  elsif actor_class.ancestors.include? Legion::Extensions::Actors::Subscription
286
326
  hook_subscription_actors_pooled([extension_hash])
287
327
  else
@@ -368,6 +408,7 @@ module Legion
368
408
 
369
409
  begin
370
410
  entry[:instance].activate if entry[:instance].respond_to?(:activate)
411
+ @running_instances << entry[:instance]
371
412
  rescue StandardError => e
372
413
  ext_name = entry[:actor_hash][:extension_name]
373
414
  Legion::Logging.error "[Subscription] activate failed for #{ext_name}: #{e.message}" if defined?(Legion::Logging)
@@ -709,8 +750,11 @@ module Legion
709
750
  segments = Helpers::Segments.derive_segments(gem_name)
710
751
  tier = category == :default ? 5 : (categories.dig(category, :tier) || 5)
711
752
 
712
- # Multi-segment gem names always need nesting for correct require paths
753
+ # Multi-segment gem names: check if the gem actually uses nested directories
754
+ # (e.g. lex-agentic-memory -> agentic/memory/) or flat underscored naming
755
+ # (e.g. lex-swarm-github -> swarm_github.rb). Probe the gem's lib/ to decide.
713
756
  nesting = true if segments.length > 1
757
+ nesting = probe_nesting(gem_name, segments) if nesting && segments.length > 1
714
758
 
715
759
  if nesting
716
760
  const_path = Helpers::Segments.derive_const_path(gem_name)
@@ -725,6 +769,19 @@ module Legion
725
769
  segments: segments, const_path: const_path, require_path: require_path }
726
770
  end
727
771
 
772
+ def probe_nesting(gem_name, segments)
773
+ gem_dir = Gem::Specification.find_by_name(gem_name).gem_dir
774
+ nested_path = "#{gem_dir}/lib/legion/extensions/#{segments.join('/')}.rb"
775
+ return true if File.exist?(nested_path)
776
+
777
+ flat_path = "#{gem_dir}/lib/legion/extensions/#{segments.join('_')}.rb"
778
+ return false if File.exist?(flat_path)
779
+
780
+ true # default to nested if neither found
781
+ rescue Gem::MissingSpecError
782
+ true
783
+ end
784
+
728
785
  def default_category_registry
729
786
  {
730
787
  core: { type: :list, tier: 1 },
@@ -319,6 +319,7 @@ module Legion
319
319
  def register_logging_hooks
320
320
  return unless defined?(Legion::Transport::Connection)
321
321
  return unless Legion::Transport::Connection.session_open?
322
+ return unless Legion::Transport::Connection.respond_to?(:log_channel)
322
323
 
323
324
  log_ch = Legion::Transport::Connection.log_channel
324
325
  unless log_ch
@@ -327,7 +328,7 @@ module Legion
327
328
  end
328
329
 
329
330
  require 'legion/transport/exchanges/logging' unless defined?(Legion::Transport::Exchanges::Logging)
330
- exchange = Legion::Transport::Exchanges::Logging.new(channel: log_ch)
331
+ exchange = Legion::Transport::Exchanges::Logging.new('legion.logging', channel: log_ch)
331
332
 
332
333
  %i[fatal error warn].each do |level|
333
334
  Legion::Logging.send(:"on_#{level}") do |event|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.5.8'
4
+ VERSION = '1.5.9'
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.5.8
4
+ version: 1.5.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -225,28 +225,28 @@ dependencies:
225
225
  requirements:
226
226
  - - ">="
227
227
  - !ruby/object:Gem::Version
228
- version: 1.3.11
228
+ version: 1.3.16
229
229
  type: :runtime
230
230
  prerelease: false
231
231
  version_requirements: !ruby/object:Gem::Requirement
232
232
  requirements:
233
233
  - - ">="
234
234
  - !ruby/object:Gem::Version
235
- version: 1.3.11
235
+ version: 1.3.16
236
236
  - !ruby/object:Gem::Dependency
237
237
  name: legion-crypt
238
238
  requirement: !ruby/object:Gem::Requirement
239
239
  requirements:
240
240
  - - ">="
241
241
  - !ruby/object:Gem::Version
242
- version: 1.4.9
242
+ version: 1.4.12
243
243
  type: :runtime
244
244
  prerelease: false
245
245
  version_requirements: !ruby/object:Gem::Requirement
246
246
  requirements:
247
247
  - - ">="
248
248
  - !ruby/object:Gem::Version
249
- version: 1.4.9
249
+ version: 1.4.12
250
250
  - !ruby/object:Gem::Dependency
251
251
  name: legion-data
252
252
  requirement: !ruby/object:Gem::Requirement
@@ -295,28 +295,28 @@ dependencies:
295
295
  requirements:
296
296
  - - ">="
297
297
  - !ruby/object:Gem::Version
298
- version: 1.3.14
298
+ version: 1.3.19
299
299
  type: :runtime
300
300
  prerelease: false
301
301
  version_requirements: !ruby/object:Gem::Requirement
302
302
  requirements:
303
303
  - - ">="
304
304
  - !ruby/object:Gem::Version
305
- version: 1.3.14
305
+ version: 1.3.19
306
306
  - !ruby/object:Gem::Dependency
307
307
  name: legion-transport
308
308
  requirement: !ruby/object:Gem::Requirement
309
309
  requirements:
310
310
  - - ">="
311
311
  - !ruby/object:Gem::Version
312
- version: 1.3.11
312
+ version: 1.4.0
313
313
  type: :runtime
314
314
  prerelease: false
315
315
  version_requirements: !ruby/object:Gem::Requirement
316
316
  requirements:
317
317
  - - ">="
318
318
  - !ruby/object:Gem::Version
319
- version: 1.3.11
319
+ version: 1.4.0
320
320
  - !ruby/object:Gem::Dependency
321
321
  name: legion-tty
322
322
  requirement: !ruby/object:Gem::Requirement
@@ -576,6 +576,7 @@ files:
576
576
  - lib/legion/cli/error_handler.rb
577
577
  - lib/legion/cli/eval_command.rb
578
578
  - lib/legion/cli/failover_command.rb
579
+ - lib/legion/cli/features_command.rb
579
580
  - lib/legion/cli/function.rb
580
581
  - lib/legion/cli/gaia_command.rb
581
582
  - lib/legion/cli/generate_command.rb