legionio 1.4.70 → 1.4.71
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 +4 -4
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +20 -15
- data/lib/legion/api/llm.rb +90 -0
- data/lib/legion/api.rb +2 -0
- data/lib/legion/mcp/context_compiler.rb +142 -0
- data/lib/legion/mcp/server.rb +6 -1
- data/lib/legion/mcp/tools/discover_tools.rb +53 -0
- data/lib/legion/mcp/tools/do_action.rb +55 -0
- data/lib/legion/version.rb +1 -1
- metadata +5 -4
- data/lib/legion/cli/tty/chat_ui.rb +0 -220
- data/lib/legion/cli/tty/palette.rb +0 -81
- data/lib/legion/cli/tty/splash.rb +0 -121
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e16727aaa70a3ffc2da0e6d3f3a6bce498e0052876e280c1d6cb42d50b3d594
|
|
4
|
+
data.tar.gz: 57042f5deb5a35dd8456850606e7a1bf42e0e4d5235385a4abb7987d13831894
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2c90fb0ba93a569c0dbd6c72f6e199fcebd6201fa4d003a114bc2d0f9bced639c28364ccb92aaf78f6ea1d226d1fa78b4a1c92538fa1002274f8656f1a8e0e1
|
|
7
|
+
data.tar.gz: 47dd1c952fcf6fd5f0f70d277b7c03ced2632dfa13cf9c47db2d1e0e03d6f90c005a8f13776a03ed900cbdf2c5e27aa7ec8d62e36751260f207c561878ef78ba
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Legion Changelog
|
|
2
2
|
|
|
3
|
+
## [1.4.71] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `POST /api/llm/chat` daemon endpoint with async (202) and sync (201) response paths
|
|
7
|
+
- `ContextCompiler` module: categorizes 35 MCP tools into 9 groups with keyword matching
|
|
8
|
+
- `legion.do` meta-tool: natural language intent routing to best-matching MCP tool
|
|
9
|
+
- `legion.tools` meta-tool: compressed catalog, category browsing, and intent-matched discovery
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `ContextCompiler.build_tool_index` now handles `MCP::Tool::InputSchema` objects (not just hashes)
|
|
13
|
+
|
|
3
14
|
## [1.4.70] - 2026-03-19
|
|
4
15
|
|
|
5
16
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -9,7 +9,7 @@ The primary gem for the LegionIO framework. An extensible async job engine for s
|
|
|
9
9
|
|
|
10
10
|
**GitHub**: https://github.com/LegionIO/LegionIO
|
|
11
11
|
**Gem**: `legionio`
|
|
12
|
-
**Version**: 1.4.
|
|
12
|
+
**Version**: 1.4.70
|
|
13
13
|
**License**: Apache-2.0
|
|
14
14
|
**Docker**: `legionio/legion`
|
|
15
15
|
**Ruby**: >= 3.4
|
|
@@ -39,21 +39,26 @@ Before any Legion code loads, `exe/legion` applies three performance optimizatio
|
|
|
39
39
|
```
|
|
40
40
|
Legion.start
|
|
41
41
|
└── Legion::Service.new
|
|
42
|
-
├── 1.
|
|
43
|
-
├── 2.
|
|
44
|
-
├── 3.
|
|
45
|
-
├── 4.
|
|
46
|
-
├── 5.
|
|
47
|
-
├── 6.
|
|
48
|
-
├── 7.
|
|
49
|
-
├── 8.
|
|
50
|
-
├── 9.
|
|
51
|
-
├── 10.
|
|
52
|
-
|
|
42
|
+
├── 1. setup_logging (legion-logging)
|
|
43
|
+
├── 2. setup_settings (legion-settings, loads /etc/legionio, ~/legionio, ./settings)
|
|
44
|
+
├── 3. Legion::Crypt.start (legion-crypt, Vault connection)
|
|
45
|
+
├── 4. setup_transport (legion-transport, RabbitMQ connection)
|
|
46
|
+
├── 5. require legion-cache
|
|
47
|
+
├── 6. setup_data (legion-data, MySQL/SQLite + migrations, optional)
|
|
48
|
+
├── 7. setup_rbac (legion-rbac, optional)
|
|
49
|
+
├── 8. setup_llm (legion-llm, optional)
|
|
50
|
+
├── 9. setup_gaia (legion-gaia, cognitive layer, optional)
|
|
51
|
+
├── 10. setup_telemetry (OpenTelemetry, optional)
|
|
52
|
+
├── 11. setup_supervision (process supervision)
|
|
53
|
+
├── 12. load_extensions (two-phase: require+autobuild all, then hook_all_actors)
|
|
54
|
+
├── 13. Legion::Crypt.cs (distribute cluster secret)
|
|
55
|
+
└── 14. setup_api (start Sinatra/Puma on port 4567)
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
Each phase calls `Legion::Readiness.mark_ready(:component)`. All phases are individually toggleable via `Service.new(transport: false, ...)`.
|
|
56
59
|
|
|
60
|
+
Extension loading is two-phase: all extensions are `require`d and `autobuild` runs first, collecting actors into `@pending_actors`. After all extensions are loaded, `hook_all_actors` starts AMQP subscriptions, timers, and other actor types. This prevents race conditions where early extensions start ticking while later ones haven't loaded yet.
|
|
61
|
+
|
|
57
62
|
### Reload Sequence
|
|
58
63
|
|
|
59
64
|
`Legion.reload` shuts down all subsystems in reverse order, waits for them to drain, then re-runs setup from settings onward. Extensions and API are re-loaded fresh.
|
|
@@ -66,7 +71,7 @@ Legion (lib/legion.rb)
|
|
|
66
71
|
│ # Entry points: Legion.start, .shutdown, .reload
|
|
67
72
|
├── Process # Daemonization: PID management, signal traps (SIGINT=quit), main loop
|
|
68
73
|
├── Readiness # Startup readiness tracking
|
|
69
|
-
│ # COMPONENTS: settings, crypt, transport, cache, data, extensions, api
|
|
74
|
+
│ # COMPONENTS: settings, crypt, transport, cache, data, gaia, extensions, api
|
|
70
75
|
│ # Readiness.ready? checks all; /api/ready returns JSON status
|
|
71
76
|
├── Events # In-process pub/sub event bus
|
|
72
77
|
│ # Events.on(name) / .emit(name, **payload) / .once / .off
|
|
@@ -723,8 +728,8 @@ rack-test, rake, rspec, rubocop, rubocop-rspec, simplecov
|
|
|
723
728
|
|
|
724
729
|
```bash
|
|
725
730
|
bundle install
|
|
726
|
-
bundle exec rspec #
|
|
727
|
-
bundle exec rubocop #
|
|
731
|
+
bundle exec rspec # 1433 examples, 0 failures
|
|
732
|
+
bundle exec rubocop # 418 files, 0 offenses
|
|
728
733
|
```
|
|
729
734
|
|
|
730
735
|
Specs use `rack-test` for API testing. `Legion::JSON.load` returns symbol keys — use `body[:data]` not `body['data']` in specs.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Legion
|
|
6
|
+
class API < Sinatra::Base
|
|
7
|
+
module Routes
|
|
8
|
+
module Llm
|
|
9
|
+
def self.registered(app)
|
|
10
|
+
app.helpers do
|
|
11
|
+
define_method(:require_llm!) do
|
|
12
|
+
return if defined?(Legion::LLM) &&
|
|
13
|
+
Legion::LLM.respond_to?(:started?) &&
|
|
14
|
+
Legion::LLM.started?
|
|
15
|
+
|
|
16
|
+
halt 503, { 'Content-Type' => 'application/json' },
|
|
17
|
+
Legion::JSON.dump({ error: { code: 'llm_unavailable',
|
|
18
|
+
message: 'LLM subsystem is not available' } })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
define_method(:cache_available?) do
|
|
22
|
+
defined?(Legion::Cache) &&
|
|
23
|
+
Legion::Cache.respond_to?(:connected?) &&
|
|
24
|
+
Legion::Cache.connected?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
register_chat(app)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.register_chat(app)
|
|
32
|
+
app.post '/api/llm/chat' do # rubocop:disable Metrics/BlockLength
|
|
33
|
+
require_llm!
|
|
34
|
+
|
|
35
|
+
body = parse_request_body
|
|
36
|
+
validate_required!(body, :message)
|
|
37
|
+
|
|
38
|
+
request_id = body[:request_id] || SecureRandom.uuid
|
|
39
|
+
message = body[:message]
|
|
40
|
+
model = body[:model]
|
|
41
|
+
provider = body[:provider]
|
|
42
|
+
|
|
43
|
+
if cache_available?
|
|
44
|
+
llm = Legion::LLM
|
|
45
|
+
rc = Legion::LLM::ResponseCache
|
|
46
|
+
rc.init_request(request_id)
|
|
47
|
+
|
|
48
|
+
Thread.new do
|
|
49
|
+
session = llm.chat_direct(model: model, provider: provider)
|
|
50
|
+
response = session.ask(message)
|
|
51
|
+
rc.complete(
|
|
52
|
+
request_id,
|
|
53
|
+
response: response.content,
|
|
54
|
+
meta: {
|
|
55
|
+
model: session.model.to_s,
|
|
56
|
+
tokens_in: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
|
|
57
|
+
tokens_out: response.respond_to?(:output_tokens) ? response.output_tokens : nil
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
rc.fail_request(request_id, code: 'llm_error', message: e.message)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
json_response({ request_id: request_id, poll_key: "llm:#{request_id}:status" },
|
|
65
|
+
status_code: 202)
|
|
66
|
+
else
|
|
67
|
+
session = Legion::LLM.chat_direct(model: model, provider: provider)
|
|
68
|
+
response = session.ask(message)
|
|
69
|
+
json_response(
|
|
70
|
+
{
|
|
71
|
+
response: response.content,
|
|
72
|
+
meta: {
|
|
73
|
+
model: session.model.to_s,
|
|
74
|
+
tokens_in: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
|
|
75
|
+
tokens_out: response.respond_to?(:output_tokens) ? response.output_tokens : nil
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
status_code: 201
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
private :register_chat
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/legion/api.rb
CHANGED
|
@@ -33,6 +33,7 @@ require_relative 'api/auth_kerberos'
|
|
|
33
33
|
require_relative 'api/capacity'
|
|
34
34
|
require_relative 'api/audit'
|
|
35
35
|
require_relative 'api/metrics'
|
|
36
|
+
require_relative 'api/llm'
|
|
36
37
|
|
|
37
38
|
module Legion
|
|
38
39
|
class API < Sinatra::Base
|
|
@@ -108,6 +109,7 @@ module Legion
|
|
|
108
109
|
register Routes::Capacity
|
|
109
110
|
register Routes::Audit
|
|
110
111
|
register Routes::Metrics
|
|
112
|
+
register Routes::Llm
|
|
111
113
|
|
|
112
114
|
use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
|
|
113
115
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module ContextCompiler
|
|
6
|
+
CATEGORIES = {
|
|
7
|
+
tasks: {
|
|
8
|
+
tools: %w[legion.run_task legion.list_tasks legion.get_task legion.delete_task legion.get_task_logs],
|
|
9
|
+
summary: 'Create, list, query, and delete tasks. Run functions via dot-notation task identifiers.'
|
|
10
|
+
},
|
|
11
|
+
chains: {
|
|
12
|
+
tools: %w[legion.list_chains legion.create_chain legion.update_chain legion.delete_chain],
|
|
13
|
+
summary: 'Manage task chains - ordered sequences of tasks that execute in series.'
|
|
14
|
+
},
|
|
15
|
+
relationships: {
|
|
16
|
+
tools: %w[legion.list_relationships legion.create_relationship legion.update_relationship
|
|
17
|
+
legion.delete_relationship],
|
|
18
|
+
summary: 'Manage trigger-action relationships between functions.'
|
|
19
|
+
},
|
|
20
|
+
extensions: {
|
|
21
|
+
tools: %w[legion.list_extensions legion.get_extension legion.enable_extension
|
|
22
|
+
legion.disable_extension],
|
|
23
|
+
summary: 'Manage LEX extensions - list installed, inspect details, enable/disable.'
|
|
24
|
+
},
|
|
25
|
+
schedules: {
|
|
26
|
+
tools: %w[legion.list_schedules legion.create_schedule legion.update_schedule legion.delete_schedule],
|
|
27
|
+
summary: 'Manage scheduled tasks - cron-style recurring task execution.'
|
|
28
|
+
},
|
|
29
|
+
workers: {
|
|
30
|
+
tools: %w[legion.list_workers legion.show_worker legion.worker_lifecycle legion.worker_costs],
|
|
31
|
+
summary: 'Manage digital workers - list, inspect, lifecycle transitions, cost tracking.'
|
|
32
|
+
},
|
|
33
|
+
rbac: {
|
|
34
|
+
tools: %w[legion.rbac_check legion.rbac_assignments legion.rbac_grants],
|
|
35
|
+
summary: 'Role-based access control - check permissions, view assignments and grants.'
|
|
36
|
+
},
|
|
37
|
+
status: {
|
|
38
|
+
tools: %w[legion.get_status legion.get_config legion.team_summary legion.routing_stats],
|
|
39
|
+
summary: 'System status, configuration, team overview, and routing statistics.'
|
|
40
|
+
},
|
|
41
|
+
describe: {
|
|
42
|
+
tools: %w[legion.describe_runner],
|
|
43
|
+
summary: 'Inspect a specific runner function - parameters, return type, metadata.'
|
|
44
|
+
}
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
# Returns a compressed summary of all categories with tool counts and tool name lists.
|
|
50
|
+
# @return [Array<Hash>] array of { category:, summary:, tool_count:, tools: }
|
|
51
|
+
def compressed_catalog
|
|
52
|
+
CATEGORIES.map do |category, config|
|
|
53
|
+
tool_names = config[:tools]
|
|
54
|
+
{
|
|
55
|
+
category: category,
|
|
56
|
+
summary: config[:summary],
|
|
57
|
+
tool_count: tool_names.length,
|
|
58
|
+
tools: tool_names
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns tools for a specific category, filtered to only those present in TOOL_CLASSES.
|
|
64
|
+
# @param category_sym [Symbol] one of the CATEGORIES keys
|
|
65
|
+
# @return [Hash, nil] { category:, summary:, tools: [{ name:, description:, params: }] } or nil
|
|
66
|
+
def category_tools(category_sym)
|
|
67
|
+
config = CATEGORIES[category_sym]
|
|
68
|
+
return nil unless config
|
|
69
|
+
|
|
70
|
+
index = tool_index
|
|
71
|
+
tools = config[:tools].filter_map { |name| index[name] }
|
|
72
|
+
return nil if tools.empty?
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
category: category_sym,
|
|
76
|
+
summary: config[:summary],
|
|
77
|
+
tools: tools
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Keyword-match intent against tool names and descriptions.
|
|
82
|
+
# @param intent_string [String] natural language intent
|
|
83
|
+
# @return [Class, nil] best matching tool CLASS from Server::TOOL_CLASSES or nil
|
|
84
|
+
def match_tool(intent_string)
|
|
85
|
+
scored = scored_tools(intent_string)
|
|
86
|
+
return nil if scored.empty?
|
|
87
|
+
|
|
88
|
+
best = scored.max_by { |entry| entry[:score] }
|
|
89
|
+
return nil if best[:score].zero?
|
|
90
|
+
|
|
91
|
+
Server::TOOL_CLASSES.find { |klass| klass.tool_name == best[:name] }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns top N keyword-matched tools ranked by score.
|
|
95
|
+
# @param intent_string [String] natural language intent
|
|
96
|
+
# @param limit [Integer] max results (default 5)
|
|
97
|
+
# @return [Array<Hash>] array of { name:, description:, score: }
|
|
98
|
+
def match_tools(intent_string, limit: 5)
|
|
99
|
+
scored = scored_tools(intent_string)
|
|
100
|
+
.select { |entry| entry[:score].positive? }
|
|
101
|
+
.sort_by { |entry| -entry[:score] }
|
|
102
|
+
scored.first(limit)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns a hash keyed by tool_name with compressed param info.
|
|
106
|
+
# Memoized — call reset! to clear.
|
|
107
|
+
# @return [Hash<String, Hash>] { name:, description:, params: [String] }
|
|
108
|
+
def tool_index
|
|
109
|
+
@tool_index ||= build_tool_index
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Clears the memoized tool_index.
|
|
113
|
+
def reset!
|
|
114
|
+
@tool_index = nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_tool_index
|
|
118
|
+
Server::TOOL_CLASSES.each_with_object({}) do |klass, idx|
|
|
119
|
+
raw_schema = klass.input_schema
|
|
120
|
+
schema = raw_schema.is_a?(Hash) ? raw_schema : raw_schema.to_h
|
|
121
|
+
properties = schema[:properties] || {}
|
|
122
|
+
idx[klass.tool_name] = {
|
|
123
|
+
name: klass.tool_name,
|
|
124
|
+
description: klass.description,
|
|
125
|
+
params: properties.keys.map(&:to_s)
|
|
126
|
+
}
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def scored_tools(intent_string)
|
|
131
|
+
keywords = intent_string.downcase.split
|
|
132
|
+
return [] if keywords.empty?
|
|
133
|
+
|
|
134
|
+
tool_index.values.map do |entry|
|
|
135
|
+
haystack = "#{entry[:name].downcase} #{entry[:description].downcase}"
|
|
136
|
+
score = keywords.count { |kw| haystack.include?(kw) }
|
|
137
|
+
{ name: entry[:name], description: entry[:description], score: score }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/legion/mcp/server.rb
CHANGED
|
@@ -33,6 +33,9 @@ require_relative 'tools/routing_stats'
|
|
|
33
33
|
require_relative 'tools/rbac_check'
|
|
34
34
|
require_relative 'tools/rbac_assignments'
|
|
35
35
|
require_relative 'tools/rbac_grants'
|
|
36
|
+
require_relative 'context_compiler'
|
|
37
|
+
require_relative 'tools/do_action'
|
|
38
|
+
require_relative 'tools/discover_tools'
|
|
36
39
|
require_relative 'resources/runner_catalog'
|
|
37
40
|
require_relative 'resources/extension_info'
|
|
38
41
|
|
|
@@ -72,7 +75,9 @@ module Legion
|
|
|
72
75
|
Tools::RoutingStats,
|
|
73
76
|
Tools::RbacCheck,
|
|
74
77
|
Tools::RbacAssignments,
|
|
75
|
-
Tools::RbacGrants
|
|
78
|
+
Tools::RbacGrants,
|
|
79
|
+
Tools::DoAction,
|
|
80
|
+
Tools::DiscoverTools
|
|
76
81
|
].freeze
|
|
77
82
|
|
|
78
83
|
class << self
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class DiscoverTools < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.tools'
|
|
8
|
+
description 'Discover available Legion tools by category or intent. Returns compressed definitions to reduce context.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
category: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Tool category: tasks, chains, relationships, extensions, schedules, workers, rbac, status, describe'
|
|
15
|
+
},
|
|
16
|
+
intent: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description: 'Describe what you want to do and relevant tools will be ranked'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def call(category: nil, intent: nil)
|
|
25
|
+
if category
|
|
26
|
+
result = ContextCompiler.category_tools(category.to_sym)
|
|
27
|
+
return error_response("Unknown category: #{category}") if result.nil?
|
|
28
|
+
|
|
29
|
+
text_response(result)
|
|
30
|
+
elsif intent
|
|
31
|
+
results = ContextCompiler.match_tools(intent, limit: 5)
|
|
32
|
+
text_response({ matched_tools: results })
|
|
33
|
+
else
|
|
34
|
+
text_response(ContextCompiler.compressed_catalog)
|
|
35
|
+
end
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
error_response("Failed: #{e.message}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def text_response(data)
|
|
43
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def error_response(msg)
|
|
47
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module MCP
|
|
5
|
+
module Tools
|
|
6
|
+
class DoAction < ::MCP::Tool
|
|
7
|
+
tool_name 'legion.do'
|
|
8
|
+
description 'Execute a Legion action by describing what you want to do in natural language. Routes to the best matching tool automatically.'
|
|
9
|
+
|
|
10
|
+
input_schema(
|
|
11
|
+
properties: {
|
|
12
|
+
intent: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Natural language description (e.g., "list all running tasks")'
|
|
15
|
+
},
|
|
16
|
+
params: {
|
|
17
|
+
type: 'object',
|
|
18
|
+
description: 'Parameters to pass to the matched tool',
|
|
19
|
+
additionalProperties: true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
required: ['intent']
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def call(intent:, params: {})
|
|
27
|
+
matched = ContextCompiler.match_tool(intent)
|
|
28
|
+
return error_response("No matching tool found for intent: #{intent}") if matched.nil?
|
|
29
|
+
|
|
30
|
+
Legion::MCP::Observer.record_intent(intent, matched) if defined?(Legion::MCP::Observer)
|
|
31
|
+
|
|
32
|
+
tool_params = params.transform_keys(&:to_sym)
|
|
33
|
+
if tool_params.empty?
|
|
34
|
+
matched.call
|
|
35
|
+
else
|
|
36
|
+
matched.call(**tool_params)
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
error_response("Failed: #{e.message}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def text_response(data)
|
|
45
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump(data) }])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def error_response(msg)
|
|
49
|
+
::MCP::Tool::Response.new([{ type: 'text', text: Legion::JSON.dump({ error: msg }) }], error: true)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/legion/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.4.71
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -353,6 +353,7 @@ files:
|
|
|
353
353
|
- lib/legion/api/gaia.rb
|
|
354
354
|
- lib/legion/api/helpers.rb
|
|
355
355
|
- lib/legion/api/hooks.rb
|
|
356
|
+
- lib/legion/api/llm.rb
|
|
356
357
|
- lib/legion/api/metrics.rb
|
|
357
358
|
- lib/legion/api/middleware/api_version.rb
|
|
358
359
|
- lib/legion/api/middleware/auth.rb
|
|
@@ -508,9 +509,6 @@ files:
|
|
|
508
509
|
- lib/legion/cli/theme.rb
|
|
509
510
|
- lib/legion/cli/trace_command.rb
|
|
510
511
|
- lib/legion/cli/trigger.rb
|
|
511
|
-
- lib/legion/cli/tty/chat_ui.rb
|
|
512
|
-
- lib/legion/cli/tty/palette.rb
|
|
513
|
-
- lib/legion/cli/tty/splash.rb
|
|
514
512
|
- lib/legion/cli/tty_command.rb
|
|
515
513
|
- lib/legion/cli/update_command.rb
|
|
516
514
|
- lib/legion/cli/version.rb
|
|
@@ -560,6 +558,7 @@ files:
|
|
|
560
558
|
- lib/legion/lex.rb
|
|
561
559
|
- lib/legion/mcp.rb
|
|
562
560
|
- lib/legion/mcp/auth.rb
|
|
561
|
+
- lib/legion/mcp/context_compiler.rb
|
|
563
562
|
- lib/legion/mcp/resources/extension_info.rb
|
|
564
563
|
- lib/legion/mcp/resources/runner_catalog.rb
|
|
565
564
|
- lib/legion/mcp/server.rb
|
|
@@ -573,6 +572,8 @@ files:
|
|
|
573
572
|
- lib/legion/mcp/tools/delete_task.rb
|
|
574
573
|
- lib/legion/mcp/tools/describe_runner.rb
|
|
575
574
|
- lib/legion/mcp/tools/disable_extension.rb
|
|
575
|
+
- lib/legion/mcp/tools/discover_tools.rb
|
|
576
|
+
- lib/legion/mcp/tools/do_action.rb
|
|
576
577
|
- lib/legion/mcp/tools/enable_extension.rb
|
|
577
578
|
- lib/legion/mcp/tools/get_config.rb
|
|
578
579
|
- lib/legion/mcp/tools/get_extension.rb
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'tty-box'
|
|
4
|
-
require 'tty-markdown'
|
|
5
|
-
require 'tty-reader'
|
|
6
|
-
require 'tty-screen'
|
|
7
|
-
require 'tty-cursor'
|
|
8
|
-
require_relative 'palette'
|
|
9
|
-
|
|
10
|
-
module Legion
|
|
11
|
-
module CLI
|
|
12
|
-
module TTY
|
|
13
|
-
module ChatUI
|
|
14
|
-
SLASH_COMMANDS = {
|
|
15
|
-
'/help' => 'Show available commands',
|
|
16
|
-
'/quit' => 'Exit chat',
|
|
17
|
-
'/clear' => 'Clear conversation',
|
|
18
|
-
'/status' => 'Show system status',
|
|
19
|
-
'/cost' => 'Show session token usage',
|
|
20
|
-
'/model' => 'Switch model',
|
|
21
|
-
'/compact' => 'Compact conversation history'
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
class << self
|
|
25
|
-
def run
|
|
26
|
-
p = Palette
|
|
27
|
-
reader = ::TTY::Reader.new(interrupt: :exit, track_history: true)
|
|
28
|
-
|
|
29
|
-
render_chat_header
|
|
30
|
-
puts
|
|
31
|
-
render_welcome
|
|
32
|
-
puts
|
|
33
|
-
|
|
34
|
-
token_count = 0
|
|
35
|
-
turn = 0
|
|
36
|
-
|
|
37
|
-
loop do
|
|
38
|
-
prompt_text = " #{p.fg(:cardinal)}\u276f#{p.reset} "
|
|
39
|
-
input = reader.read_line(prompt_text)&.strip
|
|
40
|
-
|
|
41
|
-
break if input.nil?
|
|
42
|
-
next if input.empty?
|
|
43
|
-
break if input == '/quit'
|
|
44
|
-
|
|
45
|
-
result = handle_slash_command(input, turn, token_count)
|
|
46
|
-
if result
|
|
47
|
-
turn = result[:turn] if result.key?(:turn)
|
|
48
|
-
token_count = result[:token_count] if result.key?(:token_count)
|
|
49
|
-
next
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
turn += 1
|
|
53
|
-
token_count += input.split.size * 3
|
|
54
|
-
|
|
55
|
-
response = simulate_response(input, turn)
|
|
56
|
-
token_count += response.split.size * 4
|
|
57
|
-
|
|
58
|
-
render_response(response)
|
|
59
|
-
puts
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
puts
|
|
63
|
-
puts " #{p.muted('Session ended.')} #{p.disabled("#{turn} turns, ~#{token_count} tokens")}"
|
|
64
|
-
puts
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def handle_slash_command(input, turn, token_count)
|
|
70
|
-
cursor = ::TTY::Cursor
|
|
71
|
-
case input
|
|
72
|
-
when '/help'
|
|
73
|
-
render_help
|
|
74
|
-
{}
|
|
75
|
-
when '/clear'
|
|
76
|
-
print cursor.clear_screen + cursor.move_to(0, 0)
|
|
77
|
-
render_chat_header
|
|
78
|
-
puts
|
|
79
|
-
render_system_message('Conversation cleared.')
|
|
80
|
-
puts
|
|
81
|
-
{ turn: 0, token_count: 0 }
|
|
82
|
-
when '/status'
|
|
83
|
-
render_status(turn, token_count)
|
|
84
|
-
{}
|
|
85
|
-
when '/cost'
|
|
86
|
-
render_cost(token_count)
|
|
87
|
-
{}
|
|
88
|
-
else
|
|
89
|
-
if input.start_with?('/')
|
|
90
|
-
render_system_message("Unknown command: #{input}. Type /help for available commands.")
|
|
91
|
-
puts
|
|
92
|
-
{}
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def render_chat_header
|
|
98
|
-
p = Palette
|
|
99
|
-
width = [::TTY::Screen.width, 80].min
|
|
100
|
-
|
|
101
|
-
puts " #{p.border('─' * (width - 4))}"
|
|
102
|
-
puts " #{p.heading('Legion Chat')} #{p.muted('(TTY Toolkit POC)')}"
|
|
103
|
-
puts " #{p.border('─' * (width - 4))}"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def render_welcome
|
|
107
|
-
p = Palette
|
|
108
|
-
puts " #{p.body('Type a message to chat. Use')} #{p.accent('/help')} #{p.body('for commands.')}"
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def render_help
|
|
112
|
-
p = Palette
|
|
113
|
-
puts
|
|
114
|
-
puts " #{p.heading('Commands')}"
|
|
115
|
-
puts
|
|
116
|
-
SLASH_COMMANDS.each do |cmd, desc|
|
|
117
|
-
puts " #{p.accent(cmd.ljust(12))} #{p.body(desc)}"
|
|
118
|
-
end
|
|
119
|
-
puts
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def render_system_message(text)
|
|
123
|
-
p = Palette
|
|
124
|
-
puts " #{p.muted("\u00b7")} #{p.body(text)}"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def render_status(turn, tokens)
|
|
128
|
-
p = Palette
|
|
129
|
-
puts
|
|
130
|
-
|
|
131
|
-
w = 48
|
|
132
|
-
lines = [
|
|
133
|
-
"#{p.label('Turns')} #{p.body(turn.to_s)}",
|
|
134
|
-
"#{p.label('Tokens')} #{p.body("~#{tokens}")}",
|
|
135
|
-
"#{p.label('Model')} #{p.body('claude-opus-4-6')}",
|
|
136
|
-
"#{p.label('Provider')} #{p.body('anthropic')}",
|
|
137
|
-
"#{p.label('Session')} #{p.success('active')}"
|
|
138
|
-
]
|
|
139
|
-
|
|
140
|
-
puts " #{p.border('┌')} #{p.heading('Status')} #{p.border('─' * (w - 12))}#{p.border('┐')}"
|
|
141
|
-
puts " #{p.border('│')}#{' ' * w}#{p.border('│')}"
|
|
142
|
-
lines.each do |line|
|
|
143
|
-
puts " #{p.border('│')} #{line}#{' ' * 4}#{p.border('│')}"
|
|
144
|
-
end
|
|
145
|
-
puts " #{p.border('│')}#{' ' * w}#{p.border('│')}"
|
|
146
|
-
puts " #{p.border('└')}#{p.border('─' * w)}#{p.border('┘')}"
|
|
147
|
-
puts
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def render_cost(tokens)
|
|
151
|
-
p = Palette
|
|
152
|
-
cost_estimate = (tokens / 1000.0 * 0.015).round(4)
|
|
153
|
-
puts
|
|
154
|
-
puts " #{p.label('Tokens')} #{p.body("~#{tokens}")} #{p.muted('|')} #{p.label('Cost')} #{p.body("~$#{cost_estimate}")}"
|
|
155
|
-
puts
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def render_response(text)
|
|
159
|
-
puts
|
|
160
|
-
|
|
161
|
-
# Render as markdown
|
|
162
|
-
rendered = ::TTY::Markdown.parse(
|
|
163
|
-
text,
|
|
164
|
-
width: [::TTY::Screen.width - 6, 74].min,
|
|
165
|
-
theme: {
|
|
166
|
-
em: :italic,
|
|
167
|
-
header: %i[bold],
|
|
168
|
-
hr: :dim,
|
|
169
|
-
link: [:underline],
|
|
170
|
-
list: [],
|
|
171
|
-
strong: [:bold],
|
|
172
|
-
table: [],
|
|
173
|
-
quote: [:italic]
|
|
174
|
-
}
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
rendered.each_line do |line|
|
|
178
|
-
puts " #{line}"
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def simulate_response(_input, turn)
|
|
183
|
-
responses = [
|
|
184
|
-
"I can help with that. Here's what I found:\n\n" \
|
|
185
|
-
'The LegionIO extension system uses **auto-discovery** via `Bundler.load.specs` ' \
|
|
186
|
-
"to find all `lex-*` gems. Each extension defines:\n\n" \
|
|
187
|
-
"- **Runners** — the actual functions that execute\n" \
|
|
188
|
-
"- **Actors** — execution modes (subscription, polling, interval)\n" \
|
|
189
|
-
"- **Helpers** — shared utilities for the extension\n\n" \
|
|
190
|
-
"```ruby\nmodule Legion::Extensions::MyExtension\n module Runners\n module Process\n " \
|
|
191
|
-
"def handle(payload)\n # Your logic here\n end\n end\n end\nend\n```\n\n" \
|
|
192
|
-
'Would you like me to scaffold a new extension?',
|
|
193
|
-
|
|
194
|
-
"Looking at the current GAIA tick cycle, here's the phase breakdown:\n\n" \
|
|
195
|
-
"| Phase | Name | Purpose |\n" \
|
|
196
|
-
"|-------|------|---------|\n" \
|
|
197
|
-
"| 1 | sensory_input | Gather raw input signals |\n" \
|
|
198
|
-
"| 2 | perception | Pattern recognition |\n" \
|
|
199
|
-
"| 3 | memory_retrieval | Query lex-memory traces |\n" \
|
|
200
|
-
"| 4 | knowledge_retrieval | Query Apollo knowledge base |\n" \
|
|
201
|
-
"| 5 | working_memory | Integrate context |\n\n" \
|
|
202
|
-
"The tick cycle runs at **configurable intervals** via `legion-gaia` settings.\n\n" \
|
|
203
|
-
'> Note: Apollo knowledge retrieval requires a running PostgreSQL instance with pgvector.',
|
|
204
|
-
|
|
205
|
-
"Here's a quick summary of what changed:\n\n" \
|
|
206
|
-
"### Modified Files\n\n" \
|
|
207
|
-
"1. `lib/legion/cli/tty/splash.rb` — New splash screen with TTY toolkit\n" \
|
|
208
|
-
"2. `lib/legion/cli/tty/chat_ui.rb` — Chat mode proof of concept\n" \
|
|
209
|
-
"3. `lib/legion/cli/tty/palette.rb` — Pastel-based palette wrapper\n\n" \
|
|
210
|
-
"All rendering uses the **17-shade single-hue** palette. No colors outside the system.\n\n" \
|
|
211
|
-
"```bash\nbundle exec exe/legion-tty\n```"
|
|
212
|
-
]
|
|
213
|
-
|
|
214
|
-
responses[(turn - 1) % responses.length]
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
end
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Legion
|
|
4
|
-
module CLI
|
|
5
|
-
module TTY
|
|
6
|
-
module Palette
|
|
7
|
-
# LegionIO canonical palette: 17 shades, one hue, no exceptions.
|
|
8
|
-
COLORS = {
|
|
9
|
-
void: [7, 6, 15],
|
|
10
|
-
background: [14, 13, 26],
|
|
11
|
-
deep: [18, 16, 41],
|
|
12
|
-
core_shell: [24, 22, 58],
|
|
13
|
-
glow_center: [26, 22, 64],
|
|
14
|
-
guide_rings: [30, 28, 58],
|
|
15
|
-
core_mid: [33, 30, 80],
|
|
16
|
-
skip: [42, 39, 96],
|
|
17
|
-
inner_tier: [49, 46, 128],
|
|
18
|
-
mid_arcs: [61, 56, 138],
|
|
19
|
-
diagonal_nodes: [74, 68, 168],
|
|
20
|
-
cardinal: [95, 87, 196],
|
|
21
|
-
mid_nodes: [127, 119, 221],
|
|
22
|
-
inner_nodes: [139, 131, 230],
|
|
23
|
-
innermost: [160, 154, 232],
|
|
24
|
-
near_white: [184, 178, 239],
|
|
25
|
-
self_point: [197, 194, 245]
|
|
26
|
-
}.freeze
|
|
27
|
-
|
|
28
|
-
RESET = "\e[0m"
|
|
29
|
-
BOLD = "\e[1m"
|
|
30
|
-
DIM = "\e[2m"
|
|
31
|
-
|
|
32
|
-
class << self
|
|
33
|
-
def c(name, text)
|
|
34
|
-
rgb = COLORS[name]
|
|
35
|
-
return text.to_s unless rgb
|
|
36
|
-
|
|
37
|
-
"#{fg(name)}#{text}#{RESET}"
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def bold(name, text)
|
|
41
|
-
rgb = COLORS[name]
|
|
42
|
-
return text.to_s unless rgb
|
|
43
|
-
|
|
44
|
-
"#{BOLD}#{fg(name)}#{text}#{RESET}"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def dim(name, text)
|
|
48
|
-
rgb = COLORS[name]
|
|
49
|
-
return text.to_s unless rgb
|
|
50
|
-
|
|
51
|
-
"#{DIM}#{fg(name)}#{text}#{RESET}"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def fg(name)
|
|
55
|
-
rgb = COLORS[name]
|
|
56
|
-
return '' unless rgb
|
|
57
|
-
|
|
58
|
-
"\e[38;2;#{rgb[0]};#{rgb[1]};#{rgb[2]}m"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def reset
|
|
62
|
-
RESET
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Semantic shortcuts
|
|
66
|
-
def title(text) = bold(:self_point, text)
|
|
67
|
-
def heading(text) = bold(:near_white, text)
|
|
68
|
-
def body(text) = c(:inner_nodes, text)
|
|
69
|
-
def label(text) = c(:cardinal, text)
|
|
70
|
-
def accent(text) = c(:mid_nodes, text)
|
|
71
|
-
def muted(text) = c(:diagonal_nodes, text)
|
|
72
|
-
def disabled(text) = c(:skip, text)
|
|
73
|
-
def border(text) = c(:inner_tier, text)
|
|
74
|
-
def success(text) = c(:cardinal, text)
|
|
75
|
-
def caution(text) = c(:innermost, text)
|
|
76
|
-
def critical(text) = bold(:self_point, text)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'tty-box'
|
|
4
|
-
require 'tty-progressbar'
|
|
5
|
-
require 'tty-screen'
|
|
6
|
-
require 'tty-font'
|
|
7
|
-
require 'tty-cursor'
|
|
8
|
-
require_relative 'palette'
|
|
9
|
-
|
|
10
|
-
module Legion
|
|
11
|
-
module CLI
|
|
12
|
-
module TTY
|
|
13
|
-
module Splash
|
|
14
|
-
BOOT_PHASES = [
|
|
15
|
-
{ name: 'settings', label: 'legion-settings', version: '1.3.2', delay: 0.15 },
|
|
16
|
-
{ name: 'crypt', label: 'legion-crypt', version: '1.4.3', delay: 0.20 },
|
|
17
|
-
{ name: 'transport', label: 'legion-transport', version: '1.2.1', delay: 0.30 },
|
|
18
|
-
{ name: 'cache', label: 'legion-cache', version: '1.3.0', delay: 0.15 },
|
|
19
|
-
{ name: 'data', label: 'legion-data', version: '1.4.2', delay: 0.20 },
|
|
20
|
-
{ name: 'llm', label: 'legion-llm', version: '0.3.3', delay: 0.15 },
|
|
21
|
-
{ name: 'gaia', label: 'legion-gaia', version: '0.8.0', delay: 0.10 }
|
|
22
|
-
].freeze
|
|
23
|
-
|
|
24
|
-
EXTENSIONS = %w[
|
|
25
|
-
lex-node lex-health lex-tasker lex-scheduler lex-telemetry
|
|
26
|
-
lex-memory lex-coldstart lex-apollo lex-dream lex-reflection
|
|
27
|
-
lex-perception lex-attention lex-emotion lex-motivation
|
|
28
|
-
].freeze
|
|
29
|
-
|
|
30
|
-
class << self
|
|
31
|
-
def run(version: '0.0.0')
|
|
32
|
-
cursor = ::TTY::Cursor
|
|
33
|
-
print cursor.hide
|
|
34
|
-
|
|
35
|
-
render_banner(version)
|
|
36
|
-
puts
|
|
37
|
-
boot_core_libraries
|
|
38
|
-
puts
|
|
39
|
-
load_extensions
|
|
40
|
-
puts
|
|
41
|
-
render_ready_line(version)
|
|
42
|
-
puts
|
|
43
|
-
|
|
44
|
-
print cursor.show
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def render_banner(version)
|
|
50
|
-
p = Palette
|
|
51
|
-
width = [::TTY::Screen.width, 60].min
|
|
52
|
-
|
|
53
|
-
font = ::TTY::Font.new(:standard)
|
|
54
|
-
ascii_lines = font.write('LEGION').split("\n")
|
|
55
|
-
|
|
56
|
-
# Gradient the ASCII art across palette shades
|
|
57
|
-
gradient = %i[inner_tier cardinal mid_nodes inner_nodes innermost near_white]
|
|
58
|
-
|
|
59
|
-
puts
|
|
60
|
-
ascii_lines.each_with_index do |line, i|
|
|
61
|
-
shade = gradient[i % gradient.size]
|
|
62
|
-
puts " #{p.c(shade, line)}"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
puts " #{p.border('─' * (width - 4))}"
|
|
66
|
-
puts " #{p.accent('Async Job Engine & Cognitive Mesh')} #{p.muted("v#{version}")}"
|
|
67
|
-
puts " #{p.border('─' * (width - 4))}"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def boot_core_libraries
|
|
71
|
-
p = Palette
|
|
72
|
-
puts " #{p.heading('Core Libraries')}"
|
|
73
|
-
puts
|
|
74
|
-
|
|
75
|
-
BOOT_PHASES.each do |phase|
|
|
76
|
-
puts " #{p.success('✔')} #{p.label(phase[:label].ljust(20))} #{p.muted(phase[:version])} #{p.success('ready')}"
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def load_extensions
|
|
81
|
-
p = Palette
|
|
82
|
-
puts " #{p.heading('Extensions')} #{p.muted("(#{EXTENSIONS.size} discovered)")}"
|
|
83
|
-
puts
|
|
84
|
-
|
|
85
|
-
bar = ::TTY::ProgressBar.new(
|
|
86
|
-
" #{p.fg(:cardinal)}:bar#{p.reset} :current/:total #{p.fg(:diagonal_nodes)}:eta#{p.reset}",
|
|
87
|
-
total: EXTENSIONS.size,
|
|
88
|
-
width: 30,
|
|
89
|
-
complete: "\u2588",
|
|
90
|
-
incomplete: "\u2591",
|
|
91
|
-
head: "\u2588",
|
|
92
|
-
output: $stdout
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
EXTENSIONS.each { |_ext| bar.advance(1) }
|
|
96
|
-
|
|
97
|
-
puts
|
|
98
|
-
EXTENSIONS.each_slice(4) do |group|
|
|
99
|
-
line = group.map { |ext| p.muted(ext.ljust(18)) }.join
|
|
100
|
-
puts " #{line}"
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def render_ready_line(version)
|
|
105
|
-
p = Palette
|
|
106
|
-
width = [::TTY::Screen.width, 60].min
|
|
107
|
-
|
|
108
|
-
puts " #{p.border('─' * (width - 4))}"
|
|
109
|
-
|
|
110
|
-
content = "#{p.success('Ready')} #{p.body("#{EXTENSIONS.size} extensions")} " \
|
|
111
|
-
"#{p.muted('|')} #{p.body("#{BOOT_PHASES.size} libraries")} " \
|
|
112
|
-
"#{p.muted('|')} #{p.accent("v#{version}")}"
|
|
113
|
-
puts " #{p.border('┌')}#{p.border('─' * (width - 6))}#{p.border('┐')}"
|
|
114
|
-
puts " #{p.border('│')} #{content} #{p.border('│')}"
|
|
115
|
-
puts " #{p.border('└')}#{p.border('─' * (width - 6))}#{p.border('┘')}"
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|