legionio 1.9.30 → 1.9.33

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/Gemfile +29 -53
  4. data/lib/legion/api/identity_audit.rb +20 -0
  5. data/lib/legion/cli/chat/extension_tool_loader.rb +1 -1
  6. data/lib/legion/cli/chat/permissions.rb +6 -7
  7. data/lib/legion/cli/chat/tool_registry.rb +83 -93
  8. data/lib/legion/cli/chat/tools/arbitrage_status.rb +13 -12
  9. data/lib/legion/cli/chat/tools/budget_status.rb +20 -19
  10. data/lib/legion/cli/chat/tools/consolidate_memory.rb +14 -11
  11. data/lib/legion/cli/chat/tools/cost_summary.rb +18 -16
  12. data/lib/legion/cli/chat/tools/detect_anomalies.rb +13 -11
  13. data/lib/legion/cli/chat/tools/edit_file.rb +16 -12
  14. data/lib/legion/cli/chat/tools/entity_extract.rb +18 -24
  15. data/lib/legion/cli/chat/tools/escalation_status.rb +13 -12
  16. data/lib/legion/cli/chat/tools/generate_insights.rb +23 -24
  17. data/lib/legion/cli/chat/tools/graph_explore.rb +17 -19
  18. data/lib/legion/cli/chat/tools/ingest_knowledge.rb +17 -14
  19. data/lib/legion/cli/chat/tools/knowledge_maintenance.rb +16 -12
  20. data/lib/legion/cli/chat/tools/knowledge_stats.rb +8 -9
  21. data/lib/legion/cli/chat/tools/list_extensions.rb +17 -15
  22. data/lib/legion/cli/chat/tools/manage_schedules.rb +21 -19
  23. data/lib/legion/cli/chat/tools/manage_tasks.rb +27 -36
  24. data/lib/legion/cli/chat/tools/memory_status.rb +23 -23
  25. data/lib/legion/cli/chat/tools/model_comparison.rb +18 -21
  26. data/lib/legion/cli/chat/tools/provider_health.rb +22 -19
  27. data/lib/legion/cli/chat/tools/query_knowledge.rb +15 -11
  28. data/lib/legion/cli/chat/tools/read_file.rb +12 -6
  29. data/lib/legion/cli/chat/tools/reflect.rb +19 -16
  30. data/lib/legion/cli/chat/tools/relate_knowledge.rb +16 -12
  31. data/lib/legion/cli/chat/tools/run_command.rb +17 -13
  32. data/lib/legion/cli/chat/tools/save_memory.rb +13 -10
  33. data/lib/legion/cli/chat/tools/scheduling_status.rb +16 -15
  34. data/lib/legion/cli/chat/tools/search_content.rb +12 -6
  35. data/lib/legion/cli/chat/tools/search_files.rb +11 -5
  36. data/lib/legion/cli/chat/tools/search_memory.rb +12 -8
  37. data/lib/legion/cli/chat/tools/search_traces.rb +31 -27
  38. data/lib/legion/cli/chat/tools/shadow_eval_status.rb +15 -14
  39. data/lib/legion/cli/chat/tools/spawn_agent.rb +12 -8
  40. data/lib/legion/cli/chat/tools/summarize_traces.rb +14 -11
  41. data/lib/legion/cli/chat/tools/system_status.rb +10 -11
  42. data/lib/legion/cli/chat/tools/trigger_dream.rb +19 -17
  43. data/lib/legion/cli/chat/tools/view_events.rb +14 -12
  44. data/lib/legion/cli/chat/tools/view_trends.rb +18 -18
  45. data/lib/legion/cli/chat/tools/web_search.rb +11 -5
  46. data/lib/legion/cli/chat/tools/worker_status.rb +19 -18
  47. data/lib/legion/cli/chat/tools/write_file.rb +11 -5
  48. data/lib/legion/cli/generate_command.rb +13 -6
  49. data/lib/legion/cli/setup_command.rb +1 -1
  50. data/lib/legion/extensions/helpers/base.rb +24 -8
  51. data/lib/legion/extensions.rb +49 -0
  52. data/lib/legion/identity/process.rb +50 -36
  53. data/lib/legion/version.rb +1 -1
  54. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba59d5cee637b2d8c6e78dfeb20d481f432d65e22ef4d76346f1aa6539a86752
4
- data.tar.gz: 8d80bd7e851ecf13e2a9f798c4465a2d5e7f9e21110b82b9ebf08cef6bf87901
3
+ metadata.gz: a0e7ab0c576dcf1950fb2dd7d38d1113dfd5f584883cd30286cd990f851ce6a3
4
+ data.tar.gz: a4dc69a591dd00dbb757f5e1bc5b8913005e374fed607b63d5f28088e51018a1
5
5
  SHA512:
6
- metadata.gz: a12da66d229b6a3d6c1bf6cb84d84cc09d97e0b773220faafa5d15d73d57796d7c655dec36b6f258661c2a71c4f883897cba18ba7c111ee446db46792e65c3c3
7
- data.tar.gz: 9dc487e8582733e0a5cf0fec2e57c6d80fe0f78641ce3ba0e9b816d17747cbb99c78ae051c6ab59b53ab294af0304968fdf077ce85504dd29ee6403b1ea88e7c
6
+ metadata.gz: ef50ae89f614aeec8385558444afb27f57aa1797e415ce25361ec57dec5d87ecf1181fdaf9e59bf0e696cc4ec8d68c0aa9c641bb30b1c01c08a72dd5792d0e6e
7
+ data.tar.gz: e9049722257ccdc3415ec4e33af0100d692dbf85806cd2c1fb1f4094c1189364d575943e0f39907bcc295ec7fecabfd8065aae57c0857997a22dd0904ea958ea
data/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.9.33] - 2026-05-15
6
+
7
+ ### Added
8
+ - `Legion::Identity::Process` stores and exposes `db_principal_id` and `db_identity_id` integer PKs — present in `EMPTY_STATE`, persisted through `bind!`, and included in `identity_hash`. Both default to nil until an identity provider populates them.
9
+
10
+ ## [1.9.32] - 2026-05-14
11
+
12
+ ### Removed
13
+ - Removed `gem 'ruby_llm'` dependency from Gemfile; all 40 CLI chat tools now use `Legion::Tools::Base` natively.
14
+
15
+ ### Changed
16
+ - Migrated all 40 CLI chat tool classes from `RubyLLM::Tool` to `Legion::Tools::Base`:
17
+ - `param` DSL replaced with `input_schema` (JSON Schema hash)
18
+ - `def execute` instance method replaced with `def self.call` class method
19
+ - Explicit `tool_name 'legion.<snake_case>'` added to each tool
20
+ - Private instance helpers converted to class methods
21
+ - Updated `tool_registry.rb`: removed `require 'ruby_llm'` and `begin/rescue LoadError` guard.
22
+ - Updated `extension_tool_loader.rb`: `klass < RubyLLM::Tool` changed to `klass < Legion::Tools::Base`.
23
+ - Updated `generate_command.rb` tool template to emit `Legion::Tools::Base` with `input_schema` and `def self.call`.
24
+ - `Permissions::Gate` now prepends on the singleton class to intercept `self.call` correctly.
25
+
26
+ ## [1.9.31] - 2026-05-14
27
+
28
+ ### Added
29
+ - `GET /api/identity` endpoint returning live process identity, provider resolution status, and registered provider metadata.
30
+ - `autobuild_submodules` recursive walk in `Legion::Extensions` — nested sub-modules (e.g. `Delegated`, `Application`, `ManagedIdentity`, `WorkloadIdentity` inside `lex-identity-entra`) now have their actors autobuilt and started.
31
+
32
+ ### Fixed
33
+ - `Extensions::Helpers::Base#full_path` now walks up gem name segments to find the parent gem when a sub-module gem doesn't exist as a standalone gem (e.g. `lex-identity-entra-delegated`).
34
+
5
35
  ## [1.9.30] - 2026-05-12
6
36
 
7
37
  ### Changed
data/Gemfile CHANGED
@@ -3,64 +3,41 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
-
7
- def local_gem_version(path, version_file)
8
- version_path = File.expand_path(File.join(path, version_file), __dir__)
9
- return unless File.file?(version_path)
10
-
11
- version_source = File.read(version_path)
12
- version_source[/VERSION\s*=\s*['"]([^'"]+)['"]/, 1]
13
- end
14
-
15
- def local_gem_satisfies?(path, version_file, requirement)
16
- version = local_gem_version(path, version_file)
17
- version && Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(version))
18
- end
19
-
20
- def local_gem_path(name, default_path, version_file, requirement)
21
- env_name = "#{name.upcase.tr('-', '_')}_PATH"
22
- env_path = ENV.fetch(env_name, nil)
23
- return env_path if env_path && File.exist?(File.expand_path(env_path, __dir__))
24
-
25
- return unless File.exist?(File.expand_path(default_path, __dir__))
26
- return unless local_gem_satisfies?(default_path, version_file, requirement)
27
-
28
- default_path
29
- end
30
-
31
- gem 'legion-apollo', path: '../legion-apollo' if File.exist?(File.expand_path('../legion-apollo', __dir__))
32
- gem 'legion-data', path: '../legion-data' if File.exist?(File.expand_path('../legion-data', __dir__))
33
- gem 'legion-logging', path: '../legion-logging' if File.exist?(File.expand_path('../legion-logging', __dir__))
34
- gem 'legion-settings', path: '../legion-settings' if File.exist?(File.expand_path('../legion-settings', __dir__))
35
- if (legion_tty_path = local_gem_path('legion-tty', '../legion-tty', 'lib/legion/tty/version.rb', '>= 0.5.4'))
36
- gem 'legion-tty', path: legion_tty_path
37
- end
38
-
39
- gem 'legion-gaia', path: '../legion-gaia' if File.exist?(File.expand_path('../legion-gaia', __dir__))
40
- if (legion_llm_path = local_gem_path('legion-llm', '../legion-llm', 'lib/legion/llm/version.rb', '>= 0.8.47'))
41
- gem 'legion-llm', path: legion_llm_path
42
- end
43
- gem 'legion-mcp', path: '../legion-mcp' if File.exist?(File.expand_path('../legion-mcp', __dir__))
44
-
45
- gem 'lex-kerberos'
46
-
47
- gem 'lex-apollo', path: '../extensions/lex-apollo' if File.exist?(File.expand_path('../extensions/lex-apollo', __dir__))
48
- gem 'lex-llm', path: '../extensions-ai/lex-llm' if File.exist?(File.expand_path('../extensions-ai/lex-llm', __dir__))
49
- gem 'lex-llm-ledger', path: '../extensions-ai/lex-llm-ledger' if File.exist?(File.expand_path('../extensions-ai/lex-llm-ledger', __dir__))
50
-
51
- %w[anthropic azure-foundry bedrock gemini mlx ollama openai vertex vllm].each do |provider|
52
- provider_path = "../extensions-ai/lex-llm-#{provider}"
53
- gem "lex-llm-#{provider}", path: provider_path if File.exist?(File.expand_path(provider_path, __dir__))
54
- end
55
-
56
- # gem 'lex-microsoft_teams', path: '../extensions/lex-microsoft_teams' if File.exist?(File.expand_path('../extensions/lex-microsoft_teams', __dir__))
57
-
58
6
  gem 'pg'
59
7
 
60
8
  gem 'kramdown', '>= 2.0'
61
9
  gem 'mysql2'
62
10
 
63
11
  group :test do
12
+ gem 'legion-data', path: '../legion-data' if File.exist?(File.expand_path('../legion-data', __dir__))
13
+ gem 'legion-logging', path: '../legion-logging' if File.exist?(File.expand_path('../legion-logging', __dir__))
14
+ gem 'legion-settings', path: '../legion-settings' if File.exist?(File.expand_path('../legion-settings', __dir__))
15
+
16
+ gem 'legion-apollo', path: '../legion-apollo' if File.exist?(File.expand_path('../legion-apollo', __dir__))
17
+ gem 'legion-gaia', path: '../legion-gaia' if File.exist?(File.expand_path('../legion-gaia', __dir__))
18
+ gem 'legion-llm', path: '../legion-llm' if File.exist?(File.expand_path('../legion-llm', __dir__))
19
+ gem 'legion-mcp', path: '../legion-mcp' if File.exist?(File.expand_path('../legion-mcp', __dir__))
20
+ gem 'legion-tty', path: '../legion-tty' if File.exist?(File.expand_path('../legion-tty', __dir__))
21
+
22
+ gem 'lex-apollo', path: '../extensions/lex-apollo' if File.exist?(File.expand_path('../extensions/lex-apollo', __dir__))
23
+ gem 'lex-llm', path: '../extensions-ai/lex-llm' if File.exist?(File.expand_path('../extensions-ai/lex-llm', __dir__))
24
+ gem 'lex-llm-ledger', path: '../extensions-ai/lex-llm-ledger' if File.exist?(File.expand_path('../extensions-ai/lex-llm-ledger', __dir__))
25
+
26
+ if File.exist?(File.expand_path('../extensions-identity/lex-identity-entra', __dir__))
27
+ gem 'lex-identity-entra', path: '../extensions-identity/lex-identity-entra'
28
+ end
29
+ if File.exist?(File.expand_path('../extensions-identity/lex-identity-kerberos', __dir__))
30
+ gem 'lex-identity-kerberos', path: '../extensions-identity/lex-identity-kerberos'
31
+ end
32
+ if File.exist?(File.expand_path('../extensions-identity/lex-identity-system', __dir__))
33
+ gem 'lex-identity-system', path: '../extensions-identity/lex-identity-system'
34
+ end
35
+
36
+ %w[anthropic azure-foundry bedrock gemini mlx ollama openai vertex vllm].each do |provider|
37
+ provider_path = "../extensions-ai/lex-llm-#{provider}"
38
+ gem "lex-llm-#{provider}", path: provider_path if File.exist?(File.expand_path(provider_path, __dir__))
39
+ end
40
+
64
41
  gem 'faraday'
65
42
  gem 'faraday-net_http'
66
43
  gem 'graphql'
@@ -72,6 +49,5 @@ group :test do
72
49
  gem 'rubocop'
73
50
  gem 'rubocop-legion'
74
51
  gem 'rubocop-rspec'
75
- gem 'ruby_llm'
76
52
  gem 'simplecov'
77
53
  end
@@ -7,6 +7,26 @@ module Legion
7
7
  def self.registered(app)
8
8
  app.helpers IdentityAuditHelpers
9
9
 
10
+ app.get '/api/identity' do
11
+ identity = defined?(Legion::Identity::Process) ? Legion::Identity::Process.identity_hash : {}
12
+
13
+ registered_providers = if defined?(Legion::Identity::Resolver)
14
+ Legion::Identity::Resolver.providers.map do |p|
15
+ {
16
+ name: p.provider_name,
17
+ type: p.provider_type,
18
+ trust_level: p.trust_level,
19
+ priority: p.respond_to?(:priority) ? p.priority : nil,
20
+ capabilities: p.respond_to?(:capabilities) ? p.capabilities : []
21
+ }
22
+ end
23
+ else
24
+ []
25
+ end
26
+
27
+ json_response(identity.merge(registered_providers: registered_providers))
28
+ end
29
+
10
30
  app.get '/api/identity/audit' do
11
31
  require_data!
12
32
  halt 503, json_error('unavailable', 'identity audit log not available') unless defined?(Legion::Data::Model::Identity::AuditLog)
@@ -24,7 +24,7 @@ module Legion
24
24
  def collect_tool_classes(tools_module)
25
25
  tools_module.constants.filter_map do |const_name|
26
26
  klass = tools_module.const_get(const_name)
27
- klass if klass.is_a?(Class) && klass < RubyLLM::Tool
27
+ klass if klass.is_a?(Class) && klass < Legion::Tools::Base
28
28
  end
29
29
  end
30
30
 
@@ -58,16 +58,15 @@ module Legion
58
58
  def apply!(tool_classes)
59
59
  tool_classes.each do |klass|
60
60
  tier = tier_for(klass)
61
- klass.prepend(Gate) unless tier == :read
61
+ klass.singleton_class.prepend(Gate) unless tier == :read
62
62
  end
63
63
  end
64
64
  end
65
65
 
66
66
  module Gate
67
- def call(args)
68
- normalized = normalize_args(args)
69
- desc = permission_description(normalized)
70
- return 'Tool execution denied by user.' unless Permissions.confirm?(desc)
67
+ def call(**args)
68
+ desc = permission_description(args)
69
+ return error_response('Tool execution denied by user.') unless Permissions.confirm?(desc)
71
70
 
72
71
  super
73
72
  end
@@ -75,11 +74,11 @@ module Legion
75
74
  private
76
75
 
77
76
  def permission_description(args)
78
- tier = Permissions.tier_for(self.class)
77
+ tier = Permissions.tier_for(self)
79
78
  case tier
80
79
  when :write
81
80
  path = args[:path] || '(unknown)'
82
- action = self.class.name.split('::').last.gsub(/([a-z])([A-Z])/, '\1 \2')
81
+ action = name.split('::').last.gsub(/([a-z])([A-Z])/, '\1 \2')
83
82
  "#{action}: #{path}"
84
83
  when :shell
85
84
  "Run command: #{args[:command]}"
@@ -2,52 +2,46 @@
2
2
 
3
3
  require 'legion/cli/chat_command'
4
4
 
5
- begin
6
- require 'ruby_llm'
7
-
8
- require 'legion/cli/chat/tools/read_file'
9
- require 'legion/cli/chat/tools/write_file'
10
- require 'legion/cli/chat/tools/edit_file'
11
- require 'legion/cli/chat/tools/search_files'
12
- require 'legion/cli/chat/tools/search_content'
13
- require 'legion/cli/chat/tools/run_command'
14
- require 'legion/cli/chat/tools/save_memory'
15
- require 'legion/cli/chat/tools/search_memory'
16
- require 'legion/cli/chat/tools/web_search'
17
- require 'legion/cli/chat/tools/spawn_agent'
18
- require 'legion/cli/chat/tools/search_traces'
19
- require 'legion/cli/chat/tools/query_knowledge'
20
- require 'legion/cli/chat/tools/ingest_knowledge'
21
- require 'legion/cli/chat/tools/consolidate_memory'
22
- require 'legion/cli/chat/tools/relate_knowledge'
23
- require 'legion/cli/chat/tools/knowledge_maintenance'
24
- require 'legion/cli/chat/tools/knowledge_stats'
25
- require 'legion/cli/chat/tools/summarize_traces'
26
- require 'legion/cli/chat/tools/list_extensions'
27
- require 'legion/cli/chat/tools/manage_tasks'
28
- require 'legion/cli/chat/tools/system_status'
29
- require 'legion/cli/chat/tools/view_events'
30
- require 'legion/cli/chat/tools/cost_summary'
31
- require 'legion/cli/chat/tools/reflect'
32
- require 'legion/cli/chat/tools/manage_schedules'
33
- require 'legion/cli/chat/tools/worker_status'
34
- require 'legion/cli/chat/tools/detect_anomalies'
35
- require 'legion/cli/chat/tools/view_trends'
36
- require 'legion/cli/chat/tools/trigger_dream'
37
- require 'legion/cli/chat/tools/generate_insights'
38
- require 'legion/cli/chat/tools/budget_status'
39
- require 'legion/cli/chat/tools/provider_health'
40
- require 'legion/cli/chat/tools/model_comparison'
41
- require 'legion/cli/chat/tools/shadow_eval_status'
42
- require 'legion/cli/chat/tools/entity_extract'
43
- require 'legion/cli/chat/tools/arbitrage_status'
44
- require 'legion/cli/chat/tools/escalation_status'
45
- require 'legion/cli/chat/tools/graph_explore'
46
- require 'legion/cli/chat/tools/scheduling_status'
47
- require 'legion/cli/chat/tools/memory_status'
48
- rescue LoadError => e
49
- Legion::Logging.debug("ToolRegistry ruby_llm not available, chat tools will not be registered: #{e.message}") if defined?(Legion::Logging)
50
- end
5
+ require 'legion/cli/chat/tools/read_file'
6
+ require 'legion/cli/chat/tools/write_file'
7
+ require 'legion/cli/chat/tools/edit_file'
8
+ require 'legion/cli/chat/tools/search_files'
9
+ require 'legion/cli/chat/tools/search_content'
10
+ require 'legion/cli/chat/tools/run_command'
11
+ require 'legion/cli/chat/tools/save_memory'
12
+ require 'legion/cli/chat/tools/search_memory'
13
+ require 'legion/cli/chat/tools/web_search'
14
+ require 'legion/cli/chat/tools/spawn_agent'
15
+ require 'legion/cli/chat/tools/search_traces'
16
+ require 'legion/cli/chat/tools/query_knowledge'
17
+ require 'legion/cli/chat/tools/ingest_knowledge'
18
+ require 'legion/cli/chat/tools/consolidate_memory'
19
+ require 'legion/cli/chat/tools/relate_knowledge'
20
+ require 'legion/cli/chat/tools/knowledge_maintenance'
21
+ require 'legion/cli/chat/tools/knowledge_stats'
22
+ require 'legion/cli/chat/tools/summarize_traces'
23
+ require 'legion/cli/chat/tools/list_extensions'
24
+ require 'legion/cli/chat/tools/manage_tasks'
25
+ require 'legion/cli/chat/tools/system_status'
26
+ require 'legion/cli/chat/tools/view_events'
27
+ require 'legion/cli/chat/tools/cost_summary'
28
+ require 'legion/cli/chat/tools/reflect'
29
+ require 'legion/cli/chat/tools/manage_schedules'
30
+ require 'legion/cli/chat/tools/worker_status'
31
+ require 'legion/cli/chat/tools/detect_anomalies'
32
+ require 'legion/cli/chat/tools/view_trends'
33
+ require 'legion/cli/chat/tools/trigger_dream'
34
+ require 'legion/cli/chat/tools/generate_insights'
35
+ require 'legion/cli/chat/tools/budget_status'
36
+ require 'legion/cli/chat/tools/provider_health'
37
+ require 'legion/cli/chat/tools/model_comparison'
38
+ require 'legion/cli/chat/tools/shadow_eval_status'
39
+ require 'legion/cli/chat/tools/entity_extract'
40
+ require 'legion/cli/chat/tools/arbitrage_status'
41
+ require 'legion/cli/chat/tools/escalation_status'
42
+ require 'legion/cli/chat/tools/graph_explore'
43
+ require 'legion/cli/chat/tools/scheduling_status'
44
+ require 'legion/cli/chat/tools/memory_status'
51
45
 
52
46
  require 'legion/cli/chat/permissions'
53
47
 
@@ -55,54 +49,50 @@ module Legion
55
49
  module CLI
56
50
  class Chat
57
51
  module ToolRegistry
58
- BUILTIN_TOOLS = if defined?(Tools::ReadFile)
59
- [
60
- Tools::ReadFile,
61
- Tools::WriteFile,
62
- Tools::EditFile,
63
- Tools::SearchFiles,
64
- Tools::SearchContent,
65
- Tools::RunCommand,
66
- Tools::SaveMemory,
67
- Tools::SearchMemory,
68
- Tools::WebSearch,
69
- Tools::SpawnAgent,
70
- Tools::SearchTraces,
71
- Tools::QueryKnowledge,
72
- Tools::IngestKnowledge,
73
- Tools::ConsolidateMemory,
74
- Tools::RelateKnowledge,
75
- Tools::KnowledgeMaintenance,
76
- Tools::KnowledgeStats,
77
- Tools::SummarizeTraces,
78
- Tools::ListExtensions,
79
- Tools::ManageTasks,
80
- Tools::SystemStatus,
81
- Tools::ViewEvents,
82
- Tools::CostSummary,
83
- Tools::Reflect,
84
- Tools::ManageSchedules,
85
- Tools::WorkerStatus,
86
- Tools::DetectAnomalies,
87
- Tools::ViewTrends,
88
- Tools::TriggerDream,
89
- Tools::GenerateInsights,
90
- Tools::BudgetStatus,
91
- Tools::ProviderHealth,
92
- Tools::ModelComparison,
93
- Tools::ShadowEvalStatus,
94
- Tools::EntityExtract,
95
- Tools::ArbitrageStatus,
96
- Tools::EscalationStatus,
97
- Tools::GraphExplore,
98
- Tools::SchedulingStatus,
99
- Tools::MemoryStatus
100
- ].freeze
101
- else
102
- [].freeze
103
- end
52
+ BUILTIN_TOOLS = [
53
+ Tools::ReadFile,
54
+ Tools::WriteFile,
55
+ Tools::EditFile,
56
+ Tools::SearchFiles,
57
+ Tools::SearchContent,
58
+ Tools::RunCommand,
59
+ Tools::SaveMemory,
60
+ Tools::SearchMemory,
61
+ Tools::WebSearch,
62
+ Tools::SpawnAgent,
63
+ Tools::SearchTraces,
64
+ Tools::QueryKnowledge,
65
+ Tools::IngestKnowledge,
66
+ Tools::ConsolidateMemory,
67
+ Tools::RelateKnowledge,
68
+ Tools::KnowledgeMaintenance,
69
+ Tools::KnowledgeStats,
70
+ Tools::SummarizeTraces,
71
+ Tools::ListExtensions,
72
+ Tools::ManageTasks,
73
+ Tools::SystemStatus,
74
+ Tools::ViewEvents,
75
+ Tools::CostSummary,
76
+ Tools::Reflect,
77
+ Tools::ManageSchedules,
78
+ Tools::WorkerStatus,
79
+ Tools::DetectAnomalies,
80
+ Tools::ViewTrends,
81
+ Tools::TriggerDream,
82
+ Tools::GenerateInsights,
83
+ Tools::BudgetStatus,
84
+ Tools::ProviderHealth,
85
+ Tools::ModelComparison,
86
+ Tools::ShadowEvalStatus,
87
+ Tools::EntityExtract,
88
+ Tools::ArbitrageStatus,
89
+ Tools::EscalationStatus,
90
+ Tools::GraphExplore,
91
+ Tools::SchedulingStatus,
92
+ Tools::MemoryStatus
93
+ ].freeze
104
94
 
105
- Permissions.apply!(BUILTIN_TOOLS) unless BUILTIN_TOOLS.empty?
95
+ Permissions.apply!(BUILTIN_TOOLS)
106
96
 
107
97
  def self.builtin_tools
108
98
  BUILTIN_TOOLS.dup
@@ -4,17 +4,20 @@ module Legion
4
4
  module CLI
5
5
  class Chat
6
6
  module Tools
7
- class ArbitrageStatus < RubyLLM::Tool
7
+ class ArbitrageStatus < Legion::Tools::Base
8
+ tool_name 'legion.arbitrage_status'
8
9
  description 'Show LLM cost arbitrage status: model pricing table, cheapest model per capability tier'
9
-
10
- param :capability,
11
- type: :string,
12
- desc: 'Capability tier to check: basic, moderate, or reasoning (default: show all)',
13
- required: false
10
+ input_schema({
11
+ type: 'object',
12
+ properties: {
13
+ capability: { type: 'string', description: 'Capability tier to check: basic, moderate, or reasoning (default: show all)' }
14
+ },
15
+ required: []
16
+ })
14
17
 
15
18
  TIERS = %i[basic moderate reasoning].freeze
16
19
 
17
- def execute(capability: nil)
20
+ def self.call(capability: nil)
18
21
  return 'LLM arbitrage module not available.' unless arbitrage_available?
19
22
 
20
23
  if capability
@@ -24,13 +27,11 @@ module Legion
24
27
  end
25
28
  end
26
29
 
27
- private
28
-
29
- def arbitrage_available?
30
+ def self.arbitrage_available?
30
31
  defined?(Legion::LLM::Arbitrage)
31
32
  end
32
33
 
33
- def format_overview
34
+ def self.format_overview
34
35
  arb = Legion::LLM::Arbitrage
35
36
  lines = ["LLM Cost Arbitrage\n"]
36
37
  lines << format(' Enabled: %<v>s', v: arb.enabled? ? 'YES' : 'no')
@@ -56,7 +57,7 @@ module Legion
56
57
  lines.join("\n")
57
58
  end
58
59
 
59
- def format_tier(tier)
60
+ def self.format_tier(tier)
60
61
  arb = Legion::LLM::Arbitrage
61
62
  return format('Invalid tier: %<t>s. Use: %<valid>s', t: tier, valid: TIERS.join(', ')) unless TIERS.include?(tier)
62
63
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_llm'
4
-
5
3
  begin
6
4
  require 'legion/cli/chat_command'
7
5
  rescue LoadError
@@ -12,15 +10,20 @@ module Legion
12
10
  module CLI
13
11
  class Chat
14
12
  module Tools
15
- class BudgetStatus < RubyLLM::Tool
13
+ class BudgetStatus < Legion::Tools::Base
14
+ tool_name 'legion.budget_status'
16
15
  description 'Check the current LLM session cost budget status. Shows how much has been spent, ' \
17
16
  'remaining budget, and whether the budget guard is enforcing limits. Works locally ' \
18
17
  'without needing the Legion daemon. Use this when the user asks about spending or budget.'
19
- param :action, type: 'string',
20
- desc: 'Action: "status" (default), "summary" (cost breakdown by model)',
21
- required: false
22
-
23
- def execute(action: 'status')
18
+ input_schema({
19
+ type: 'object',
20
+ properties: {
21
+ action: { type: 'string', description: 'Action: "status" (default), "summary" (cost breakdown by model)' }
22
+ },
23
+ required: []
24
+ })
25
+
26
+ def self.call(action: 'status')
24
27
  return 'Legion::LLM not available.' unless llm_available?
25
28
 
26
29
  case action.to_s
@@ -32,9 +35,7 @@ module Legion
32
35
  "Error checking budget: #{e.message}"
33
36
  end
34
37
 
35
- private
36
-
37
- def format_status
38
+ def self.format_status
38
39
  guard = budget_guard_status
39
40
  tracker = cost_summary
40
41
  lines = ["Session Budget Status:\n"]
@@ -49,7 +50,7 @@ module Legion
49
50
  lines.join("\n")
50
51
  end
51
52
 
52
- def format_summary
53
+ def self.format_summary
53
54
  tracker = cost_summary
54
55
  return 'No LLM requests recorded this session.' if tracker[:total_requests].zero?
55
56
 
@@ -63,7 +64,7 @@ module Legion
63
64
  lines.join("\n")
64
65
  end
65
66
 
66
- def append_model_breakdown(lines, by_model)
67
+ def self.append_model_breakdown(lines, by_model)
67
68
  return unless by_model&.any?
68
69
 
69
70
  lines << "\n By Model:"
@@ -73,31 +74,31 @@ module Legion
73
74
  end
74
75
  end
75
76
 
76
- def budget_guard_status
77
+ def self.budget_guard_status
77
78
  return { enforcing: false, budget_usd: 0.0, ratio: 0.0 } unless budget_guard_available?
78
79
 
79
80
  Legion::LLM::Hooks::BudgetGuard.status
80
81
  end
81
82
 
82
- def cost_summary
83
+ def self.cost_summary
83
84
  return empty_summary unless cost_tracker_available?
84
85
 
85
86
  Legion::LLM::CostTracker.summary
86
87
  end
87
88
 
88
- def budget_guard_available?
89
+ def self.budget_guard_available?
89
90
  defined?(Legion::LLM::Hooks::BudgetGuard)
90
91
  end
91
92
 
92
- def cost_tracker_available?
93
+ def self.cost_tracker_available?
93
94
  defined?(Legion::LLM::CostTracker)
94
95
  end
95
96
 
96
- def llm_available?
97
+ def self.llm_available?
97
98
  defined?(Legion::LLM)
98
99
  end
99
100
 
100
- def empty_summary
101
+ def self.empty_summary
101
102
  { total_cost_usd: 0.0, total_requests: 0, total_input_tokens: 0, total_output_tokens: 0, by_model: {} }
102
103
  end
103
104
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_llm'
4
-
5
3
  begin
6
4
  require 'legion/cli/chat_command'
7
5
  require 'legion/cli/chat/memory_store'
@@ -13,12 +11,19 @@ module Legion
13
11
  module CLI
14
12
  class Chat
15
13
  module Tools
16
- class ConsolidateMemory < RubyLLM::Tool
14
+ class ConsolidateMemory < Legion::Tools::Base
15
+ tool_name 'legion.consolidate_memory'
17
16
  description 'Consolidate and organize memory entries by removing duplicates, merging related items, ' \
18
17
  'and creating cleaner summaries. Use this when memory has grown cluttered or has redundant entries. ' \
19
18
  'Pass scope "project" or "global" to target the right memory file.'
20
- param :scope, type: 'string', desc: 'Memory scope: "project" or "global" (default: project)'
21
- param :dry_run, type: 'string', desc: 'Set to "true" to preview without writing (default: false)', required: false
19
+ input_schema({
20
+ type: 'object',
21
+ properties: {
22
+ scope: { type: 'string', description: 'Memory scope: "project" or "global" (default: project)' },
23
+ dry_run: { type: 'string', description: 'Set to "true" to preview without writing (default: false)' }
24
+ },
25
+ required: []
26
+ })
22
27
 
23
28
  CONSOLIDATION_PROMPT = <<~PROMPT
24
29
  You are a memory consolidation engine. Given a list of memory entries, produce a cleaned-up version that:
@@ -34,7 +39,7 @@ module Legion
34
39
  Do NOT add headers, explanations, or commentary.
35
40
  PROMPT
36
41
 
37
- def execute(scope: 'project', dry_run: nil)
42
+ def self.call(scope: 'project', dry_run: nil)
38
43
  dry_run = dry_run.to_s == 'true'
39
44
  scope_sym = scope.to_s == 'global' ? :global : :project
40
45
 
@@ -60,9 +65,7 @@ module Legion
60
65
  "Error consolidating memory: #{e.message}"
61
66
  end
62
67
 
63
- private
64
-
65
- def consolidate_entries(entries)
68
+ def self.consolidate_entries(entries)
66
69
  return nil unless defined?(Legion::LLM) && Legion::LLM.respond_to?(:chat_direct)
67
70
 
68
71
  numbered = entries.map.with_index(1) { |e, i| "#{i}. #{e}" }.join("\n")
@@ -72,7 +75,7 @@ module Legion
72
75
  response.content
73
76
  end
74
77
 
75
- def parse_consolidated(text)
78
+ def self.parse_consolidated(text)
76
79
  text.lines
77
80
  .map(&:strip)
78
81
  .select { |line| line.start_with?('- ') }
@@ -80,7 +83,7 @@ module Legion
80
83
  .reject(&:empty?)
81
84
  end
82
85
 
83
- def write_consolidated(entries, scope_sym)
86
+ def self.write_consolidated(entries, scope_sym)
84
87
  path = scope_sym == :global ? MemoryStore.global_path : MemoryStore.project_path
85
88
  header = scope_sym == :global ? "# Global Memory\n" : "# Project Memory\n"
86
89
  timestamp = Time.now.strftime('%Y-%m-%d %H:%M')