legionio 1.4.186 → 1.4.187

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: fe7c5039053d23c8a5870b63ca0059a3c30c1112bdad029206baafa574037fa8
4
- data.tar.gz: 9e4a119425469614f68c2a4ad4d966656248ae3601850ba0841e7d31144de583
3
+ metadata.gz: b09d64e1595a3f95f937031e668f6b9a0fd53eb491a93641c4a539c6d702851b
4
+ data.tar.gz: c490032828ae6057ef5c4cfb8520c9942b7f0654e46262cc72a06aa3db078975
5
5
  SHA512:
6
- metadata.gz: 4647975c2d46807169d45b66b7dc3a45f533c7964d17113945e4ee605904efcca32d3efbe1acf061cffcbfb34fb4125c67d14522a1976d1c0824e55c7a9d9d1e
7
- data.tar.gz: 73fe8aac0e9a7e64dbf3564b7fa8c0e8427cb507649096664e40c79632aef16ed93b19684fdfcba86ce84129c44ca69077490002aa1eb226ad591a250a991f51
6
+ metadata.gz: 6c35f063fe1cbbf4f3b5867956acebe29f486a0fda4227d271fd3ae6e0b4c877329aad0d1d8e65d38b39cdca8abbb693da48bb6caab52fffe0e654489056b2df
7
+ data.tar.gz: c47560f824ba85fe4847c2992a46fbeb6c2504ecdf21de6cfd520fcc547b59de08dac53cb182113651efa316f47995f8a02cccc8ad268e5a00844eb7acc9ecfe
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Legion Changelog
2
2
 
3
+ ## [1.4.187] - 2026-03-23
4
+
5
+ ### Added
6
+ - `Legion::Extensions::Capability` Data.define struct for extension capability registration
7
+ - `Legion::Extensions::Catalog::Registry` in-memory capability registry with register, find, find_by_intent, for_mcp, for_override, find_by_mcp_name
8
+ - `register_capabilities` populates Catalog::Registry from extension runners at boot
9
+ - `unregister_capabilities` removes capabilities from Catalog on extension unload
10
+ - `Catalog::Registry.on_change` callback for notifying consumers on registry changes
11
+
3
12
  ## [1.4.186] - 2026-03-23
4
13
 
5
14
  ### Fixed
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ Capability = ::Data.define(
6
+ :name, :extension, :runner, :function,
7
+ :description, :parameters, :tags, :loaded_at
8
+ ) do
9
+ def self.from_runner(extension:, runner:, function:, **opts)
10
+ canonical = "#{extension}:#{runner.to_s.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase}:#{function}"
11
+ new(
12
+ name: canonical,
13
+ extension: extension,
14
+ runner: runner.to_s,
15
+ function: function.to_s,
16
+ description: opts[:description],
17
+ parameters: opts[:parameters] || {},
18
+ tags: Array(opts[:tags]),
19
+ loaded_at: Time.now
20
+ )
21
+ end
22
+
23
+ def matches_intent?(text)
24
+ words = text.downcase.split(/\s+/)
25
+ searchable = [description, *tags, extension, runner, function]
26
+ .compact.join(' ').downcase
27
+
28
+ matching = words.count { |w| searchable.include?(w) }
29
+ matching.to_f / [words.length, 1].max >= 0.4
30
+ end
31
+
32
+ def to_mcp_tool
33
+ snake_runner = runner.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
34
+ tool_name = "legion.#{extension.delete_prefix('lex-').tr('-', '_')}.#{snake_runner}.#{function}"
35
+ properties = (parameters || {}).transform_values do |v|
36
+ v.is_a?(Hash) ? v : { type: v.to_s }
37
+ end
38
+
39
+ {
40
+ name: tool_name,
41
+ description: description || "#{extension} #{runner}##{function}",
42
+ input_schema: {
43
+ type: 'object',
44
+ properties: properties,
45
+ required: parameters&.select { |_, v| v.is_a?(Hash) && v[:required] }&.keys&.map(&:to_s) || []
46
+ }
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Catalog
6
+ module Registry
7
+ @capabilities = []
8
+ @by_name = {}
9
+ @mutex = Mutex.new
10
+ @on_change_callbacks = []
11
+
12
+ module_function
13
+
14
+ def register(capability)
15
+ @mutex.synchronize do
16
+ return if @by_name.key?(capability.name)
17
+
18
+ @capabilities << capability
19
+ @by_name[capability.name] = capability
20
+ end
21
+ notify_change
22
+ end
23
+
24
+ def unregister(name)
25
+ @mutex.synchronize do
26
+ cap = @by_name.delete(name)
27
+ @capabilities.delete(cap) if cap
28
+ return unless cap
29
+ end
30
+ notify_change
31
+ end
32
+
33
+ def unregister_extension(extension_name)
34
+ @mutex.synchronize do
35
+ removed = @capabilities.select { |c| c.extension == extension_name }
36
+ removed.each do |cap|
37
+ @by_name.delete(cap.name)
38
+ @capabilities.delete(cap)
39
+ end
40
+ return if removed.empty?
41
+ end
42
+ notify_change
43
+ end
44
+
45
+ def capabilities
46
+ @mutex.synchronize { @capabilities.dup.freeze }
47
+ end
48
+
49
+ def find(name:)
50
+ @mutex.synchronize { @by_name[name] }
51
+ end
52
+
53
+ def find_by_intent(text)
54
+ @mutex.synchronize do
55
+ @capabilities.select { |c| c.matches_intent?(text) }
56
+ end
57
+ end
58
+
59
+ def for_mcp
60
+ @mutex.synchronize { @capabilities.dup }
61
+ end
62
+
63
+ def find_by_mcp_name(mcp_name)
64
+ @mutex.synchronize do
65
+ @capabilities.find { |cap| cap.to_mcp_tool[:name] == mcp_name }
66
+ end
67
+ end
68
+
69
+ def for_override(tool_name)
70
+ @mutex.synchronize do
71
+ normalized = tool_name.downcase.tr('-', '_')
72
+ @capabilities.find do |cap|
73
+ cap.function.downcase == normalized ||
74
+ cap.name.downcase.end_with?(normalized) ||
75
+ cap.tags.any? { |t| t.downcase == normalized }
76
+ end
77
+ end
78
+ end
79
+
80
+ def count
81
+ @mutex.synchronize { @capabilities.length }
82
+ end
83
+
84
+ def on_change(&block)
85
+ @mutex.synchronize { @on_change_callbacks << block }
86
+ end
87
+
88
+ def reset!
89
+ @mutex.synchronize do
90
+ @capabilities.clear
91
+ @by_name.clear
92
+ @on_change_callbacks.clear
93
+ end
94
+ end
95
+
96
+ def notify_change
97
+ callbacks = @mutex.synchronize { @on_change_callbacks.dup }
98
+ callbacks.each do |cb|
99
+ cb.call
100
+ rescue StandardError => e
101
+ Legion::Logging.warn("Catalog::Registry on_change error: #{e.message}") if defined?(Legion::Logging)
102
+ end
103
+ end
104
+
105
+ private_class_method :notify_change
106
+ end
107
+ end
108
+ end
109
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'catalog/registry'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Catalog
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'legion/extensions/core'
4
+ require 'legion/extensions/capability'
4
5
  require 'legion/extensions/catalog'
5
6
  require 'legion/extensions/permissions'
6
7
  require 'legion/runner'
@@ -45,7 +46,10 @@ module Legion
45
46
  @timer_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
46
47
  @poll_tasks.each { |task| task[:running_class].cancel if task[:running_class].respond_to?(:cancel) }
47
48
 
48
- @loaded_extensions.each { |name| Catalog.transition(name, :stopped) }
49
+ @loaded_extensions.each do |name|
50
+ Catalog.transition(name, :stopped)
51
+ unregister_capabilities(name)
52
+ end
49
53
  Legion::Logging.info 'Successfully shut down all actors'
50
54
  end
51
55
 
@@ -158,6 +162,8 @@ module Legion
158
162
  require 'legion/transport/messages/lex_register'
159
163
  Legion::Transport::Messages::LexRegister.new(function: 'save', opts: extension.runners).publish
160
164
 
165
+ register_capabilities(entry[:gem_name], extension.runners) if extension.respond_to?(:runners)
166
+
161
167
  if extension.respond_to?(:meta_actors) && extension.meta_actors.is_a?(Hash)
162
168
  extension.meta_actors.each_value do |actor|
163
169
  extension.log.debug("deferring meta actor: #{actor}") if has_logger
@@ -345,6 +351,36 @@ module Legion
345
351
 
346
352
  public
347
353
 
354
+ def unregister_capabilities(gem_name)
355
+ Extensions::Catalog::Registry.unregister_extension(gem_name)
356
+ end
357
+
358
+ def register_capabilities(gem_name, runners)
359
+ runners.each_value do |runner_meta|
360
+ runner_name = runner_meta[:runner_name]
361
+ (runner_meta[:class_methods] || {}).each do |fn_name, fn_meta|
362
+ next if fn_name.to_s.start_with?('_')
363
+
364
+ params = {}
365
+ (fn_meta[:args] || []).each do |arg|
366
+ type, name = arg
367
+ params[name] = { type: :string, required: type == :keyreq }
368
+ end
369
+
370
+ cap = Extensions::Capability.from_runner(
371
+ extension: gem_name,
372
+ runner: runner_name.to_s.split('_').map(&:capitalize).join,
373
+ function: fn_name.to_s,
374
+ parameters: params,
375
+ tags: [gem_name.delete_prefix('lex-')]
376
+ )
377
+ Extensions::Catalog::Registry.register(cap)
378
+ end
379
+ rescue StandardError => e
380
+ Legion::Logging.warn("Catalog registration error for #{gem_name}: #{e.message}") if defined?(Legion::Logging)
381
+ end
382
+ end
383
+
348
384
  def gem_load(entry)
349
385
  gem_name = entry[:gem_name]
350
386
  require_path = entry[:require_path]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Legion
4
- VERSION = '1.4.186'
4
+ VERSION = '1.4.187'
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.186
4
+ version: 1.4.187
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -689,7 +689,9 @@ files:
689
689
  - lib/legion/extensions/builders/hooks.rb
690
690
  - lib/legion/extensions/builders/routes.rb
691
691
  - lib/legion/extensions/builders/runners.rb
692
+ - lib/legion/extensions/capability.rb
692
693
  - lib/legion/extensions/catalog.rb
694
+ - lib/legion/extensions/catalog/registry.rb
693
695
  - lib/legion/extensions/core.rb
694
696
  - lib/legion/extensions/data.rb
695
697
  - lib/legion/extensions/data/migrator.rb