legion-tty 0.4.42 → 0.5.0
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 +22 -0
- data/lib/legion/tty/app.rb +27 -21
- data/lib/legion/tty/background/github_probe.rb +27 -24
- data/lib/legion/tty/background/llm_probe.rb +12 -8
- data/lib/legion/tty/background/scanner.rb +19 -16
- data/lib/legion/tty/components/command_palette.rb +5 -1
- data/lib/legion/tty/components/digital_rain.rb +7 -1
- data/lib/legion/tty/components/markdown_view.rb +6 -1
- data/lib/legion/tty/components/message_stream.rb +5 -2
- data/lib/legion/tty/components/model_picker.rb +5 -1
- data/lib/legion/tty/components/progress_panel.rb +4 -1
- data/lib/legion/tty/components/session_picker.rb +5 -1
- data/lib/legion/tty/components/status_bar.rb +7 -0
- data/lib/legion/tty/components/table_view.rb +7 -1
- data/lib/legion/tty/components/tool_call_parser.rb +4 -1
- data/lib/legion/tty/daemon_client.rb +16 -0
- data/lib/legion/tty/keybinding_manager.rb +4 -1
- data/lib/legion/tty/notification_gate.rb +40 -0
- data/lib/legion/tty/notify.rb +5 -1
- data/lib/legion/tty/screens/chat/export_commands.rb +8 -4
- data/lib/legion/tty/screens/chat/message_commands.rb +8 -4
- data/lib/legion/tty/screens/chat/model_commands.rb +6 -2
- data/lib/legion/tty/screens/chat/session_commands.rb +6 -2
- data/lib/legion/tty/screens/chat/ui_commands.rb +333 -6
- data/lib/legion/tty/screens/chat.rb +90 -16
- data/lib/legion/tty/screens/config.rb +6 -3
- data/lib/legion/tty/screens/dashboard.rb +26 -9
- data/lib/legion/tty/screens/extensions.rb +4 -1
- data/lib/legion/tty/screens/onboarding.rb +79 -75
- data/lib/legion/tty/session_store.rb +6 -3
- data/lib/legion/tty/version.rb +1 -1
- data/lib/legion/tty.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20065c242e16ca4262309393cdad733b473f139c1efd288512ffe9d704fb5b69
|
|
4
|
+
data.tar.gz: 1159cc15d4545b6dbda4d9a69087a9f9a929c6579fd477c1ae0a27d12dbf95c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77d12618eb36e18a62e3c0fbfa973f645155a4611771982d596b03803a1c98e3e4b592f3b40699842cd23f977ecb86518c1cdd3a26a4e58db936f0f45ba6137c
|
|
7
|
+
data.tar.gz: 4b8f2551be051f7140c851f9cac8a632430a102eda8f0ca78a5b1750fb0f2c266fc903fafd6e01606c5a2222986ab1cab3539003d41120bfac8a2762957de329
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-04-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`Legion::Tools::Registry` integration** — `/tools` now queries `Legion::Tools::Registry.all_tools` to display registered tool names, descriptions, MCP tiers, and deferred status; falls back to gem scan when Registry is unavailable (closes #15)
|
|
7
|
+
- **`/tool <name> [json-args]`** — new slash command to invoke any registered tool directly from chat; tries local Registry first, falls back to `DaemonClient.run_tool` (closes #15)
|
|
8
|
+
- **Tool schemas in inference** — `build_tool_schemas` passes always-loaded tool schemas to every `DaemonClient.inference` call so the LLM can use registered tools (closes #15)
|
|
9
|
+
- **TriggerIndex intent routing** — when `Legion::Tools::TriggerIndex` is non-empty and a user message matches a registered trigger word, routes to `Legion::Tools::Do.call` directly without an LLM round-trip (closes #15)
|
|
10
|
+
- **`DaemonClient.run_tool`** — new method: `POST /api/tools/run`; returns `{status: :ok/:error/:unavailable}` (closes #15)
|
|
11
|
+
- **`/skills`** — new slash command: lists all registered `Legion::LLM::Skills` with namespace, trigger type, trigger words, and description (closes #16)
|
|
12
|
+
- **`/skills load <path>`** — loads a `.md` skill file at runtime via `Legion::LLM::Skills::DiskLoader` (closes #16)
|
|
13
|
+
- **`/skills run <namespace>:<name>`** — invokes a registered skill and shows its injected content (closes #16)
|
|
14
|
+
- **`/apollo query/ingest/graph/status/autoingest`** — new slash command family for the `Legion::Apollo` knowledge store: semantic query with confidence-ranked results, content ingestion, graph traversal by entity ID, and live status (closes #17)
|
|
15
|
+
- **Apollo dashboard panel** — System Info panel on the dashboard now shows `Apollo: started/transport/data` availability (closes #17)
|
|
16
|
+
- **`Legion::TTY::NotificationGate`** — new module wrapping `Legion::Gaia::NotificationGate`; `status_bar.notify` now gates `info`/`success`/`warning` notifications through Gaia's presence, behavioral, and schedule evaluators; `error` level always delivers (closes #18)
|
|
17
|
+
- **`/gaia status`** — shows Gaia NotificationGate evaluator state: presence, arousal score, and quiet-hours status (closes #18)
|
|
18
|
+
- **`/gaia presence <status>`** — sets user presence (`Available`, `DoNotDisturb`, `Away`, etc.) for the session, controlling notification threshold (closes #18)
|
|
19
|
+
- **`/context` auto-inject skills** — `/context` output now includes auto-inject skill names when `Legion::LLM::Skills::Registry` is available (closes #16)
|
|
20
|
+
- **Debug mode skill count** — status bar debug segment shows `skills:N` when `Legion::LLM::Skills::Registry` is available (closes #16)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- `handle_tools` refactored into `handle_tools_registry` + `handle_tools_gem_scan` helpers for clarity
|
|
24
|
+
|
|
3
25
|
## [0.4.42] - 2026-04-08
|
|
4
26
|
|
|
5
27
|
### Changed
|
data/lib/legion/tty/app.rb
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'legion/logging'
|
|
5
6
|
require_relative 'screen_manager'
|
|
6
7
|
require_relative 'hotkeys'
|
|
8
|
+
require_relative 'notification_gate'
|
|
7
9
|
require_relative 'screens/onboarding'
|
|
8
10
|
require_relative 'screens/chat'
|
|
9
11
|
|
|
@@ -11,6 +13,12 @@ module Legion
|
|
|
11
13
|
module TTY
|
|
12
14
|
# rubocop:disable Metrics/ClassLength
|
|
13
15
|
class App
|
|
16
|
+
include Legion::Logging::Helper
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
include Legion::Logging::Helper
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
CONFIG_DIR = File.expand_path('~/.legionio/settings')
|
|
15
23
|
|
|
16
24
|
# Key normalization: raw escape sequences and control chars to symbols
|
|
@@ -34,7 +42,7 @@ module Legion
|
|
|
34
42
|
app = new(**opts)
|
|
35
43
|
app.start
|
|
36
44
|
rescue Interrupt => e
|
|
37
|
-
|
|
45
|
+
log.debug { "app interrupted: #{e.message}" }
|
|
38
46
|
app&.shutdown
|
|
39
47
|
end
|
|
40
48
|
|
|
@@ -70,7 +78,7 @@ module Legion
|
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
# Public: called by screens (e.g., Chat during LLM streaming) to force a re-render
|
|
73
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
81
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
74
82
|
def render_frame
|
|
75
83
|
width = terminal_width
|
|
76
84
|
height = terminal_height
|
|
@@ -97,9 +105,9 @@ module Legion
|
|
|
97
105
|
|
|
98
106
|
$stdout.flush
|
|
99
107
|
rescue StandardError => e
|
|
100
|
-
|
|
108
|
+
log.warn { "render_frame failed: #{e.message}" }
|
|
101
109
|
end
|
|
102
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
110
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
103
111
|
|
|
104
112
|
# Temporarily exit raw mode for blocking prompts (TTY::Prompt, etc.)
|
|
105
113
|
def with_cooked_mode(&)
|
|
@@ -316,7 +324,7 @@ module Legion
|
|
|
316
324
|
end
|
|
317
325
|
result
|
|
318
326
|
rescue StandardError => e
|
|
319
|
-
|
|
327
|
+
log.warn { "composite_overlay failed: #{e.message}" }
|
|
320
328
|
lines
|
|
321
329
|
end
|
|
322
330
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -393,7 +401,7 @@ module Legion
|
|
|
393
401
|
boot_legion_subsystems
|
|
394
402
|
@llm_chat = try_settings_llm
|
|
395
403
|
rescue StandardError => e
|
|
396
|
-
|
|
404
|
+
log.warn { "setup_llm failed: #{e.message}" }
|
|
397
405
|
@llm_chat = nil
|
|
398
406
|
end
|
|
399
407
|
|
|
@@ -416,13 +424,13 @@ module Legion
|
|
|
416
424
|
File.write(identity_path, ::JSON.generate(identity))
|
|
417
425
|
@config = load_config
|
|
418
426
|
rescue StandardError => e
|
|
419
|
-
|
|
427
|
+
log.warn { "rescan_environment failed: #{e.message}" }
|
|
420
428
|
nil
|
|
421
429
|
end
|
|
422
430
|
end
|
|
423
431
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
424
432
|
|
|
425
|
-
# rubocop:disable Metrics/AbcSize, Metrics/
|
|
433
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
426
434
|
def boot_legion_subsystems
|
|
427
435
|
require 'legion/logging'
|
|
428
436
|
Legion::Logging.setup(log_level: 'error', level: 'error', trace: false)
|
|
@@ -438,10 +446,10 @@ module Legion
|
|
|
438
446
|
Legion::Crypt.start unless Legion::Crypt.instance_variable_get(:@started)
|
|
439
447
|
Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
|
|
440
448
|
rescue LoadError => e
|
|
441
|
-
|
|
449
|
+
log.debug { "legion/crypt not available: #{e.message}" }
|
|
442
450
|
nil
|
|
443
451
|
rescue StandardError => e
|
|
444
|
-
|
|
452
|
+
log.warn { "crypt/secrets setup failed: #{e.message}" }
|
|
445
453
|
nil
|
|
446
454
|
end
|
|
447
455
|
|
|
@@ -449,14 +457,14 @@ module Legion
|
|
|
449
457
|
require 'legion/llm'
|
|
450
458
|
Legion::Settings.merge_settings(:llm, Legion::LLM::Settings.default)
|
|
451
459
|
rescue LoadError => e
|
|
452
|
-
|
|
460
|
+
log.debug { "legion/llm not available: #{e.message}" }
|
|
453
461
|
nil
|
|
454
462
|
end
|
|
455
463
|
rescue LoadError => e
|
|
456
|
-
|
|
464
|
+
log.debug { "legion subsystem load failed: #{e.message}" }
|
|
457
465
|
nil
|
|
458
466
|
end
|
|
459
|
-
# rubocop:enable Metrics/AbcSize, Metrics/
|
|
467
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
460
468
|
|
|
461
469
|
def settings_search_path
|
|
462
470
|
[
|
|
@@ -471,15 +479,13 @@ module Legion
|
|
|
471
479
|
# All LLM calls route through the LegionIO daemon API.
|
|
472
480
|
# No raw RubyLLM session is created here — nil signals "use daemon path".
|
|
473
481
|
if Legion::TTY::DaemonClient.available?
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
Legion::Logging.warn('TTY: daemon not running; LLM unavailable until daemon starts')
|
|
478
|
-
end
|
|
482
|
+
log.debug { 'TTY: daemon available, LLM routed through daemon' }
|
|
483
|
+
else
|
|
484
|
+
log.warn { 'TTY: daemon not running; LLM unavailable until daemon starts' }
|
|
479
485
|
end
|
|
480
486
|
nil
|
|
481
487
|
rescue StandardError => e
|
|
482
|
-
|
|
488
|
+
log.warn { "try_settings_llm failed: #{e.message}" }
|
|
483
489
|
nil
|
|
484
490
|
end
|
|
485
491
|
|
|
@@ -549,7 +555,7 @@ module Legion
|
|
|
549
555
|
|
|
550
556
|
deep_symbolize(::JSON.parse(File.read(path)))
|
|
551
557
|
rescue ::JSON::ParserError, Errno::ENOENT => e
|
|
552
|
-
|
|
558
|
+
log.warn { "load_credentials failed: #{e.message}" }
|
|
553
559
|
{}
|
|
554
560
|
end
|
|
555
561
|
|
|
@@ -566,7 +572,7 @@ module Legion
|
|
|
566
572
|
|
|
567
573
|
deep_symbolize(::JSON.parse(File.read(path)))
|
|
568
574
|
rescue ::JSON::ParserError, Errno::ENOENT => e
|
|
569
|
-
|
|
575
|
+
log.warn { "load_config failed: #{e.message}" }
|
|
570
576
|
{}
|
|
571
577
|
end
|
|
572
578
|
|
|
@@ -3,17 +3,20 @@
|
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'uri'
|
|
6
|
+
require 'legion/logging'
|
|
6
7
|
|
|
7
8
|
module Legion
|
|
8
9
|
module TTY
|
|
9
10
|
module Background
|
|
10
11
|
# rubocop:disable Metrics/ClassLength
|
|
11
12
|
class GitHubProbe
|
|
13
|
+
include Legion::Logging::Helper
|
|
14
|
+
|
|
12
15
|
API_BASE = 'https://api.github.com'
|
|
13
16
|
USER_AGENT = 'legion-tty/github-probe'
|
|
14
17
|
|
|
15
18
|
def initialize(token: nil, logger: nil)
|
|
16
|
-
@
|
|
19
|
+
@boot_log = logger
|
|
17
20
|
@token = token || resolve_token
|
|
18
21
|
end
|
|
19
22
|
|
|
@@ -21,19 +24,19 @@ module Legion
|
|
|
21
24
|
def run_quick_async(queue)
|
|
22
25
|
Thread.new do
|
|
23
26
|
unless @token
|
|
24
|
-
@
|
|
27
|
+
@boot_log&.log('github', 'quick probe skipped (no token)')
|
|
25
28
|
queue.push({ type: :github_quick_complete, data: nil })
|
|
26
29
|
return
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
@
|
|
32
|
+
@boot_log&.log('github', 'quick probe: fetching /user + recent commits')
|
|
30
33
|
t0 = Time.now
|
|
31
34
|
result = fetch_quick_profile
|
|
32
35
|
elapsed = ((Time.now - t0) * 1000).round
|
|
33
|
-
@
|
|
36
|
+
@boot_log&.log('github', "quick probe complete in #{elapsed}ms")
|
|
34
37
|
queue.push({ type: :github_quick_complete, data: result })
|
|
35
38
|
rescue StandardError => e
|
|
36
|
-
@
|
|
39
|
+
@boot_log&.log('github', "quick probe ERROR: #{e.class}: #{e.message}")
|
|
37
40
|
queue.push({ type: :github_quick_error, error: e.message })
|
|
38
41
|
end
|
|
39
42
|
end
|
|
@@ -42,9 +45,9 @@ module Legion
|
|
|
42
45
|
# rubocop:disable Metrics/AbcSize
|
|
43
46
|
def run_async(queue, remotes: [], quick_profile: nil)
|
|
44
47
|
Thread.new do
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@
|
|
48
|
+
@boot_log&.log('github', "probing with #{remotes.size} remotes: #{remotes.first(5).inspect}")
|
|
49
|
+
@boot_log&.log('github', "token: #{@token ? 'present' : 'NONE'}")
|
|
50
|
+
@boot_log&.log('github', "quick_profile: #{quick_profile ? 'reusing' : 'none'}")
|
|
48
51
|
t0 = Time.now
|
|
49
52
|
result = if @token
|
|
50
53
|
build_authenticated_result(remotes, quick_profile: quick_profile)
|
|
@@ -52,10 +55,10 @@ module Legion
|
|
|
52
55
|
build_public_result(remotes)
|
|
53
56
|
end
|
|
54
57
|
elapsed = ((Time.now - t0) * 1000).round
|
|
55
|
-
@
|
|
58
|
+
@boot_log&.log('github', "probe complete in #{elapsed}ms")
|
|
56
59
|
queue.push({ type: :github_probe_complete, data: result })
|
|
57
60
|
rescue StandardError => e
|
|
58
|
-
@
|
|
61
|
+
@boot_log&.log('github', "ERROR: #{e.class}: #{e.message}")
|
|
59
62
|
queue.push({ type: :github_error, error: e.message })
|
|
60
63
|
end
|
|
61
64
|
end
|
|
@@ -71,14 +74,14 @@ module Legion
|
|
|
71
74
|
return nil unless user_data.is_a?(Hash) && user_data['login']
|
|
72
75
|
|
|
73
76
|
username = user_data['login']
|
|
74
|
-
@
|
|
77
|
+
@boot_log&.log('github', "quick: authenticated as #{username}")
|
|
75
78
|
|
|
76
79
|
week_ago = (Time.now - (7 * 86_400)).strftime('%Y-%m-%d')
|
|
77
80
|
month_ago = (Time.now - (30 * 86_400)).strftime('%Y-%m-%d')
|
|
78
81
|
|
|
79
82
|
commits_week = count_commits(username, since: week_ago)
|
|
80
83
|
commits_month = count_commits(username, since: month_ago)
|
|
81
|
-
@
|
|
84
|
+
@boot_log&.log('github', "quick: commits this week=#{commits_week} this month=#{commits_month}")
|
|
82
85
|
|
|
83
86
|
{
|
|
84
87
|
username: username,
|
|
@@ -105,7 +108,7 @@ module Legion
|
|
|
105
108
|
|
|
106
109
|
data['total_count'] || 0
|
|
107
110
|
rescue StandardError => e
|
|
108
|
-
|
|
111
|
+
log.debug { "count_commits failed: #{e.message}" }
|
|
109
112
|
0
|
|
110
113
|
end
|
|
111
114
|
|
|
@@ -115,16 +118,16 @@ module Legion
|
|
|
115
118
|
def build_authenticated_result(remotes, quick_profile: nil)
|
|
116
119
|
if quick_profile
|
|
117
120
|
username = quick_profile[:username]
|
|
118
|
-
@
|
|
121
|
+
@boot_log&.log('github', "reusing quick profile for: #{username}")
|
|
119
122
|
profile = quick_profile
|
|
120
123
|
else
|
|
121
124
|
user_data = api_get('/user')
|
|
122
125
|
unless user_data.is_a?(Hash) && user_data['login']
|
|
123
|
-
@
|
|
126
|
+
@boot_log&.log('github', 'authenticated /user failed, falling back to public')
|
|
124
127
|
return build_public_result(remotes)
|
|
125
128
|
end
|
|
126
129
|
username = user_data['login']
|
|
127
|
-
@
|
|
130
|
+
@boot_log&.log('github', "authenticated as: #{username}")
|
|
128
131
|
profile = extract_profile(user_data)
|
|
129
132
|
end
|
|
130
133
|
orgs = fetch_orgs
|
|
@@ -227,7 +230,7 @@ module Legion
|
|
|
227
230
|
|
|
228
231
|
def build_public_result(remotes)
|
|
229
232
|
username = remotes.filter_map { |r| infer_username(r) }.first
|
|
230
|
-
@
|
|
233
|
+
@boot_log&.log('github', "inferred username: #{username || 'none'}")
|
|
231
234
|
return { username: nil } unless username
|
|
232
235
|
|
|
233
236
|
profile_data = api_get("/users/#{username}")
|
|
@@ -288,17 +291,17 @@ module Legion
|
|
|
288
291
|
ENV.fetch('GH_TOKEN', nil) ||
|
|
289
292
|
ENV.fetch('GITHUB_PERSONAL_ACCESS_TOKEN', nil)
|
|
290
293
|
if env_token
|
|
291
|
-
@
|
|
294
|
+
@boot_log&.log('github', 'token source: environment variable')
|
|
292
295
|
return env_token
|
|
293
296
|
end
|
|
294
297
|
|
|
295
298
|
gh_token = token_from_gh_cli
|
|
296
299
|
if gh_token
|
|
297
|
-
@
|
|
300
|
+
@boot_log&.log('github', 'token source: gh CLI')
|
|
298
301
|
return gh_token
|
|
299
302
|
end
|
|
300
303
|
|
|
301
|
-
@
|
|
304
|
+
@boot_log&.log('github', 'no token found (no env var, no gh CLI)')
|
|
302
305
|
nil
|
|
303
306
|
end
|
|
304
307
|
|
|
@@ -306,10 +309,10 @@ module Legion
|
|
|
306
309
|
gh_path = `which gh 2>/dev/null`.strip
|
|
307
310
|
return nil if gh_path.empty?
|
|
308
311
|
|
|
309
|
-
@
|
|
312
|
+
@boot_log&.log('github', "found gh CLI at #{gh_path}")
|
|
310
313
|
|
|
311
314
|
status = `gh auth status 2>&1`
|
|
312
|
-
@
|
|
315
|
+
@boot_log&.log('github', "gh auth status: #{status.lines.first&.strip}")
|
|
313
316
|
return nil unless $CHILD_STATUS&.success?
|
|
314
317
|
|
|
315
318
|
token = `gh auth token 2>/dev/null`.strip
|
|
@@ -317,7 +320,7 @@ module Legion
|
|
|
317
320
|
|
|
318
321
|
token
|
|
319
322
|
rescue StandardError => e
|
|
320
|
-
@
|
|
323
|
+
@boot_log&.log('github', "gh CLI error: #{e.message}")
|
|
321
324
|
nil
|
|
322
325
|
end
|
|
323
326
|
# --- HTTP ---
|
|
@@ -328,7 +331,7 @@ module Legion
|
|
|
328
331
|
response = http.request(build_request(uri))
|
|
329
332
|
::JSON.parse(response.body)
|
|
330
333
|
rescue StandardError => e
|
|
331
|
-
|
|
334
|
+
log.debug { "api_get failed: #{e.message}" }
|
|
332
335
|
nil
|
|
333
336
|
end
|
|
334
337
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module TTY
|
|
5
7
|
module Background
|
|
6
8
|
class LlmProbe
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
7
11
|
def initialize(logger: nil, wait_queue: nil)
|
|
8
|
-
@
|
|
12
|
+
@boot_log = logger
|
|
9
13
|
@wait_queue = wait_queue
|
|
10
14
|
end
|
|
11
15
|
|
|
@@ -15,7 +19,7 @@ module Legion
|
|
|
15
19
|
result = probe_providers
|
|
16
20
|
queue.push({ data: result })
|
|
17
21
|
rescue StandardError => e
|
|
18
|
-
@
|
|
22
|
+
@boot_log&.log('llm_probe', "error: #{e.message}")
|
|
19
23
|
queue.push({ data: { providers: [], error: e.message } })
|
|
20
24
|
end
|
|
21
25
|
end
|
|
@@ -36,12 +40,12 @@ module Legion
|
|
|
36
40
|
sleep 0.2
|
|
37
41
|
end
|
|
38
42
|
if timed_out
|
|
39
|
-
@
|
|
43
|
+
@boot_log&.log('llm_probe', 'bootstrap wait timed out')
|
|
40
44
|
else
|
|
41
|
-
@
|
|
45
|
+
@boot_log&.log('llm_probe', 'bootstrap wait complete')
|
|
42
46
|
end
|
|
43
47
|
rescue StandardError => e
|
|
44
|
-
@
|
|
48
|
+
@boot_log&.log('llm_probe', "bootstrap wait error: #{e.message}")
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def probe_providers
|
|
@@ -55,7 +59,7 @@ module Legion
|
|
|
55
59
|
def start_llm
|
|
56
60
|
Legion::LLM.start unless Legion::LLM.started?
|
|
57
61
|
rescue StandardError => e
|
|
58
|
-
@
|
|
62
|
+
@boot_log&.log('llm_probe', "LLM start failed: #{e.message}")
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
def collect_provider_results
|
|
@@ -64,7 +68,7 @@ module Legion
|
|
|
64
68
|
next unless config[:enabled]
|
|
65
69
|
|
|
66
70
|
result = ping_provider(name, config)
|
|
67
|
-
@
|
|
71
|
+
@boot_log&.log('llm_probe', "#{name}: #{result[:status]} (#{result[:latency_ms]}ms)")
|
|
68
72
|
result
|
|
69
73
|
end
|
|
70
74
|
end
|
|
@@ -77,7 +81,7 @@ module Legion
|
|
|
77
81
|
{ name: name, model: model, status: :ok, latency_ms: latency }
|
|
78
82
|
rescue StandardError => e
|
|
79
83
|
latency = ((Time.now - start_time) * 1000).round
|
|
80
|
-
|
|
84
|
+
log.debug { "ping_provider #{name} failed: #{e.message}" }
|
|
81
85
|
{ name: name, model: model, status: :configured, latency_ms: latency, error: e.message }
|
|
82
86
|
end
|
|
83
87
|
end
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'legion/logging'
|
|
5
6
|
|
|
6
7
|
module Legion
|
|
7
8
|
module TTY
|
|
8
9
|
module Background
|
|
9
10
|
# rubocop:disable Metrics/ClassLength
|
|
10
11
|
class Scanner
|
|
12
|
+
include Legion::Logging::Helper
|
|
13
|
+
|
|
11
14
|
MAX_DEPTH = 3
|
|
12
15
|
|
|
13
16
|
SERVICES = {
|
|
@@ -34,7 +37,7 @@ module Legion
|
|
|
34
37
|
|
|
35
38
|
def initialize(base_dirs: nil, logger: nil)
|
|
36
39
|
@base_dirs = base_dirs || [File.expand_path('~')]
|
|
37
|
-
@
|
|
40
|
+
@boot_log = logger
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
def scan_services
|
|
@@ -73,14 +76,14 @@ module Legion
|
|
|
73
76
|
|
|
74
77
|
def run_async(queue)
|
|
75
78
|
Thread.new do
|
|
76
|
-
@
|
|
79
|
+
@boot_log&.log('scanner', "starting scan of #{@base_dirs.join(', ')}")
|
|
77
80
|
t0 = Time.now
|
|
78
81
|
data = scan_all
|
|
79
82
|
elapsed = ((Time.now - t0) * 1000).round
|
|
80
|
-
@
|
|
83
|
+
@boot_log&.log('scanner', "scan complete in #{elapsed}ms")
|
|
81
84
|
queue.push({ type: :scan_complete, data: data })
|
|
82
85
|
rescue StandardError => e
|
|
83
|
-
@
|
|
86
|
+
@boot_log&.log('scanner', "ERROR: #{e.class}: #{e.message}")
|
|
84
87
|
queue.push({ type: :scan_error, error: e.message })
|
|
85
88
|
end
|
|
86
89
|
end
|
|
@@ -90,11 +93,11 @@ module Legion
|
|
|
90
93
|
def port_open?(host, port)
|
|
91
94
|
::Socket.tcp(host, port, connect_timeout: 1) { true }
|
|
92
95
|
rescue StandardError => e
|
|
93
|
-
|
|
96
|
+
log.debug { "port_open? #{host}:#{port} failed: #{e.message}" }
|
|
94
97
|
false
|
|
95
98
|
end
|
|
96
99
|
|
|
97
|
-
# rubocop:disable Metrics/AbcSize
|
|
100
|
+
# rubocop:disable Metrics/AbcSize
|
|
98
101
|
def collect_repos(base, depth = 0)
|
|
99
102
|
return [] unless File.directory?(base)
|
|
100
103
|
return [build_repo_entry(base)] if File.directory?(File.join(base, '.git'))
|
|
@@ -106,14 +109,14 @@ module Legion
|
|
|
106
109
|
child_path = File.join(base, child)
|
|
107
110
|
acc.concat(collect_repos(child_path, depth + 1)) if File.directory?(child_path)
|
|
108
111
|
rescue StandardError => e
|
|
109
|
-
|
|
112
|
+
log.debug { "collect_repos child failed: #{e.message}" }
|
|
110
113
|
next
|
|
111
114
|
end
|
|
112
115
|
rescue StandardError => e
|
|
113
|
-
|
|
116
|
+
log.debug { "collect_repos failed: #{e.message}" }
|
|
114
117
|
[]
|
|
115
118
|
end
|
|
116
|
-
# rubocop:enable Metrics/AbcSize
|
|
119
|
+
# rubocop:enable Metrics/AbcSize
|
|
117
120
|
|
|
118
121
|
def build_repo_entry(path)
|
|
119
122
|
{ path: path, name: File.basename(path), remote: git_remote(path),
|
|
@@ -124,7 +127,7 @@ module Legion
|
|
|
124
127
|
out = `git -C #{path.shellescape} remote get-url origin 2>/dev/null`.strip
|
|
125
128
|
out.empty? ? nil : out
|
|
126
129
|
rescue StandardError => e
|
|
127
|
-
|
|
130
|
+
log.debug { "git_remote failed: #{e.message}" }
|
|
128
131
|
nil
|
|
129
132
|
end
|
|
130
133
|
|
|
@@ -132,7 +135,7 @@ module Legion
|
|
|
132
135
|
out = `git -C #{path.shellescape} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
133
136
|
out.empty? ? nil : out
|
|
134
137
|
rescue StandardError => e
|
|
135
|
-
|
|
138
|
+
log.debug { "git_branch failed: #{e.message}" }
|
|
136
139
|
nil
|
|
137
140
|
end
|
|
138
141
|
|
|
@@ -152,7 +155,7 @@ module Legion
|
|
|
152
155
|
|
|
153
156
|
File.readlines(full, encoding: 'utf-8', chomp: true).last(500)
|
|
154
157
|
rescue StandardError => e
|
|
155
|
-
|
|
158
|
+
log.debug { "read_history_lines failed for #{path}: #{e.message}" }
|
|
156
159
|
[]
|
|
157
160
|
end
|
|
158
161
|
end
|
|
@@ -178,11 +181,11 @@ module Legion
|
|
|
178
181
|
result[:signing_key] = signing_key unless signing_key.empty?
|
|
179
182
|
result
|
|
180
183
|
rescue StandardError => e
|
|
181
|
-
|
|
184
|
+
log.debug { "scan_gitconfig failed: #{e.message}" }
|
|
182
185
|
nil
|
|
183
186
|
end
|
|
184
187
|
|
|
185
|
-
def scan_jfrog
|
|
188
|
+
def scan_jfrog # rubocop:disable Metrics/AbcSize
|
|
186
189
|
config_path = File.expand_path('~/.jfrog/jfrog-cli.conf.v6')
|
|
187
190
|
return nil unless File.exist?(config_path)
|
|
188
191
|
|
|
@@ -195,7 +198,7 @@ module Legion
|
|
|
195
198
|
{ server_id: s[:serverId], url: s[:url], user: s[:user] }
|
|
196
199
|
end
|
|
197
200
|
rescue StandardError => e
|
|
198
|
-
|
|
201
|
+
log.debug { "scan_jfrog failed: #{e.message}" }
|
|
199
202
|
nil
|
|
200
203
|
end
|
|
201
204
|
|
|
@@ -210,7 +213,7 @@ module Legion
|
|
|
210
213
|
|
|
211
214
|
{ hosts: hosts.map(&:to_s) }
|
|
212
215
|
rescue StandardError => e
|
|
213
|
-
|
|
216
|
+
log.debug { "scan_terraform failed: #{e.message}" }
|
|
214
217
|
nil
|
|
215
218
|
end
|
|
216
219
|
end
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
4
|
+
|
|
3
5
|
module Legion
|
|
4
6
|
module TTY
|
|
5
7
|
module Components
|
|
6
8
|
class CommandPalette
|
|
9
|
+
include Legion::Logging::Helper
|
|
10
|
+
|
|
7
11
|
COMMANDS = %w[/help /quit /clear /model /session /cost /export /tools
|
|
8
12
|
/dashboard /hotkeys /save /load /sessions /system /delete /plan
|
|
9
13
|
/palette /extensions /config].freeze
|
|
@@ -36,7 +40,7 @@ module Legion
|
|
|
36
40
|
choices = entries.map { |e| { name: "#{e[:label]} (#{e[:category]})", value: e[:label] } }
|
|
37
41
|
prompt.select('Command:', choices, filter: true, per_page: 15)
|
|
38
42
|
rescue ::TTY::Reader::InputInterrupt, Interrupt => e
|
|
39
|
-
|
|
43
|
+
log.debug { "command palette cancelled: #{e.message}" }
|
|
40
44
|
nil
|
|
41
45
|
end
|
|
42
46
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
3
4
|
require_relative '../theme'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
@@ -7,6 +8,11 @@ module Legion
|
|
|
7
8
|
module Components
|
|
8
9
|
# rubocop:disable Metrics/ClassLength
|
|
9
10
|
class DigitalRain
|
|
11
|
+
include Legion::Logging::Helper
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
include Legion::Logging::Helper
|
|
15
|
+
end
|
|
10
16
|
# rubocop:disable Naming/VariableNumber
|
|
11
17
|
FADE_SHADES = %i[
|
|
12
18
|
purple_12 purple_11 purple_10 purple_9 purple_8
|
|
@@ -40,7 +46,7 @@ module Legion
|
|
|
40
46
|
.map { |s| s.name.sub(/^lex-/, '') }
|
|
41
47
|
gems.empty? ? FALLBACK_NAMES : gems
|
|
42
48
|
rescue StandardError => e
|
|
43
|
-
|
|
49
|
+
log.debug { "extension_names failed: #{e.message}" }
|
|
44
50
|
FALLBACK_NAMES
|
|
45
51
|
end
|
|
46
52
|
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'tty-markdown'
|
|
4
|
+
require 'legion/logging'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
6
7
|
module TTY
|
|
7
8
|
module Components
|
|
8
9
|
module MarkdownView
|
|
10
|
+
class << self
|
|
11
|
+
include Legion::Logging::Helper
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
def self.render(text, width: 80)
|
|
10
15
|
::TTY::Markdown.parse(text, width: width)
|
|
11
16
|
rescue StandardError => e
|
|
12
|
-
|
|
17
|
+
log.warn { "markdown render failed: #{e.message}" }
|
|
13
18
|
"#{text}\n(markdown render error: #{e.message})"
|
|
14
19
|
end
|
|
15
20
|
end
|