legionio 1.9.31 → 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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Gemfile +0 -1
  4. data/lib/legion/cli/chat/extension_tool_loader.rb +1 -1
  5. data/lib/legion/cli/chat/permissions.rb +6 -7
  6. data/lib/legion/cli/chat/tool_registry.rb +83 -93
  7. data/lib/legion/cli/chat/tools/arbitrage_status.rb +13 -12
  8. data/lib/legion/cli/chat/tools/budget_status.rb +20 -19
  9. data/lib/legion/cli/chat/tools/consolidate_memory.rb +14 -11
  10. data/lib/legion/cli/chat/tools/cost_summary.rb +18 -16
  11. data/lib/legion/cli/chat/tools/detect_anomalies.rb +13 -11
  12. data/lib/legion/cli/chat/tools/edit_file.rb +16 -12
  13. data/lib/legion/cli/chat/tools/entity_extract.rb +18 -24
  14. data/lib/legion/cli/chat/tools/escalation_status.rb +13 -12
  15. data/lib/legion/cli/chat/tools/generate_insights.rb +23 -24
  16. data/lib/legion/cli/chat/tools/graph_explore.rb +17 -19
  17. data/lib/legion/cli/chat/tools/ingest_knowledge.rb +17 -14
  18. data/lib/legion/cli/chat/tools/knowledge_maintenance.rb +16 -12
  19. data/lib/legion/cli/chat/tools/knowledge_stats.rb +8 -9
  20. data/lib/legion/cli/chat/tools/list_extensions.rb +17 -15
  21. data/lib/legion/cli/chat/tools/manage_schedules.rb +21 -19
  22. data/lib/legion/cli/chat/tools/manage_tasks.rb +27 -36
  23. data/lib/legion/cli/chat/tools/memory_status.rb +23 -23
  24. data/lib/legion/cli/chat/tools/model_comparison.rb +18 -21
  25. data/lib/legion/cli/chat/tools/provider_health.rb +22 -19
  26. data/lib/legion/cli/chat/tools/query_knowledge.rb +15 -11
  27. data/lib/legion/cli/chat/tools/read_file.rb +12 -6
  28. data/lib/legion/cli/chat/tools/reflect.rb +19 -16
  29. data/lib/legion/cli/chat/tools/relate_knowledge.rb +16 -12
  30. data/lib/legion/cli/chat/tools/run_command.rb +17 -13
  31. data/lib/legion/cli/chat/tools/save_memory.rb +13 -10
  32. data/lib/legion/cli/chat/tools/scheduling_status.rb +16 -15
  33. data/lib/legion/cli/chat/tools/search_content.rb +12 -6
  34. data/lib/legion/cli/chat/tools/search_files.rb +11 -5
  35. data/lib/legion/cli/chat/tools/search_memory.rb +12 -8
  36. data/lib/legion/cli/chat/tools/search_traces.rb +31 -27
  37. data/lib/legion/cli/chat/tools/shadow_eval_status.rb +15 -14
  38. data/lib/legion/cli/chat/tools/spawn_agent.rb +12 -8
  39. data/lib/legion/cli/chat/tools/summarize_traces.rb +14 -11
  40. data/lib/legion/cli/chat/tools/system_status.rb +10 -11
  41. data/lib/legion/cli/chat/tools/trigger_dream.rb +19 -17
  42. data/lib/legion/cli/chat/tools/view_events.rb +14 -12
  43. data/lib/legion/cli/chat/tools/view_trends.rb +18 -18
  44. data/lib/legion/cli/chat/tools/web_search.rb +11 -5
  45. data/lib/legion/cli/chat/tools/worker_status.rb +19 -18
  46. data/lib/legion/cli/chat/tools/write_file.rb +11 -5
  47. data/lib/legion/cli/generate_command.rb +13 -6
  48. data/lib/legion/identity/process.rb +50 -36
  49. data/lib/legion/version.rb +1 -1
  50. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 765d0bb57b5a10135d49ccbaca56669a33b201118421fc90242acd4f12d752ec
4
- data.tar.gz: 464deff220c28495e29c7def972d4c5a7c925abdee6572991fd01d84e6791878
3
+ metadata.gz: a0e7ab0c576dcf1950fb2dd7d38d1113dfd5f584883cd30286cd990f851ce6a3
4
+ data.tar.gz: a4dc69a591dd00dbb757f5e1bc5b8913005e374fed607b63d5f28088e51018a1
5
5
  SHA512:
6
- metadata.gz: aba6393b1e60a435ff0e752ae6b7d5a035387a1e6012ec997d3c0f1c0ee31d408d7fb78f8ed4340e1594963b0510189047ea1fd0f6d49f18b7d914ff247fa60b
7
- data.tar.gz: dd3c0bd6abd74759c9858c8192b8938dbdf17333093e9173bdc681868c1f0fd039fb87f9659e8a0388bde76e47cfd71a97937e24f619c31a9d2b32476aded675
6
+ metadata.gz: ef50ae89f614aeec8385558444afb27f57aa1797e415ce25361ec57dec5d87ecf1181fdaf9e59bf0e696cc4ec8d68c0aa9c641bb30b1c01c08a72dd5792d0e6e
7
+ data.tar.gz: e9049722257ccdc3415ec4e33af0100d692dbf85806cd2c1fb1f4094c1189364d575943e0f39907bcc295ec7fecabfd8065aae57c0857997a22dd0904ea958ea
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
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
+
5
26
  ## [1.9.31] - 2026-05-14
6
27
 
7
28
  ### Added
data/Gemfile CHANGED
@@ -49,6 +49,5 @@ group :test do
49
49
  gem 'rubocop'
50
50
  gem 'rubocop-legion'
51
51
  gem 'rubocop-rspec'
52
- gem 'ruby_llm'
53
52
  gem 'simplecov'
54
53
  end
@@ -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')
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ruby_llm'
4
3
  require 'net/http'
5
4
  require 'json'
6
5
 
@@ -14,20 +13,25 @@ module Legion
14
13
  module CLI
15
14
  class Chat
16
15
  module Tools
17
- class CostSummary < RubyLLM::Tool
16
+ class CostSummary < Legion::Tools::Base
17
+ tool_name 'legion.cost_summary'
18
18
  description 'Get cost and token usage summary from the running Legion daemon. Shows spending ' \
19
19
  'for today, this week, and this month, plus top cost consumers by worker. ' \
20
20
  'Use this to monitor LLM spending and identify expensive operations.'
21
- param :action, type: 'string',
22
- desc: 'Action: "summary" (default), "top" (top consumers), or "worker" (specific worker)',
23
- required: false
24
- param :worker_id, type: 'string', desc: 'Worker ID (required for "worker" action)', required: false
25
- param :limit, type: 'integer', desc: 'Number of top consumers to show (default: 5)', required: false
21
+ input_schema({
22
+ type: 'object',
23
+ properties: {
24
+ action: { type: 'string', description: 'Action: "summary" (default), "top" (top consumers), or "worker" (specific worker)' },
25
+ worker_id: { type: 'string', description: 'Worker ID (required for "worker" action)' },
26
+ limit: { type: 'integer', description: 'Number of top consumers to show (default: 5)' }
27
+ },
28
+ required: []
29
+ })
26
30
 
27
31
  DEFAULT_PORT = 4567
28
32
  DEFAULT_HOST = '127.0.0.1'
29
33
 
30
- def execute(action: 'summary', worker_id: nil, limit: 5)
34
+ def self.call(action: 'summary', worker_id: nil, limit: 5)
31
35
  case action.to_s
32
36
  when 'top'
33
37
  handle_top(limit.to_i.clamp(1, 20))
@@ -45,9 +49,7 @@ module Legion
45
49
  "Error fetching cost data: #{e.message}"
46
50
  end
47
51
 
48
- private
49
-
50
- def handle_summary
52
+ def self.handle_summary
51
53
  data = api_get('/api/costs/summary?period=month')
52
54
  return "API error: #{data[:error]}" if data[:error]
53
55
 
@@ -60,7 +62,7 @@ module Legion
60
62
  lines.join("\n")
61
63
  end
62
64
 
63
- def handle_top(limit)
65
+ def self.handle_top(limit)
64
66
  data = api_get('/api/workers')
65
67
  return "API error: #{data[:error]}" if data[:error]
66
68
 
@@ -77,7 +79,7 @@ module Legion
77
79
  lines.join("\n")
78
80
  end
79
81
 
80
- def handle_worker(worker_id)
82
+ def self.handle_worker(worker_id)
81
83
  data = api_get("/api/workers/#{worker_id}/value")
82
84
  return "API error: #{data[:error]}" if data[:error]
83
85
 
@@ -91,7 +93,7 @@ module Legion
91
93
  lines.join("\n")
92
94
  end
93
95
 
94
- def fetch_worker_cost(worker_id)
96
+ def self.fetch_worker_cost(worker_id)
95
97
  data = api_get("/api/workers/#{worker_id}/value")
96
98
  data = data[:data] || data
97
99
  (data[:total_cost_usd] || 0).to_f
@@ -99,7 +101,7 @@ module Legion
99
101
  0.0
100
102
  end
101
103
 
102
- def api_get(path)
104
+ def self.api_get(path)
103
105
  uri = URI("http://#{DEFAULT_HOST}:#{api_port}#{path}")
104
106
  http = Net::HTTP.new(uri.host, uri.port)
105
107
  http.open_timeout = 2
@@ -108,7 +110,7 @@ module Legion
108
110
  ::JSON.parse(response.body, symbolize_names: true)
109
111
  end
110
112
 
111
- def api_port
113
+ def self.api_port
112
114
  return DEFAULT_PORT unless defined?(Legion::Settings)
113
115
 
114
116
  Legion::Settings[:api]&.dig(:port) || DEFAULT_PORT