legion-tty 0.4.42 → 0.5.1
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 +31 -0
- data/lib/legion/tty/app.rb +58 -24
- 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/input_bar.rb +12 -2
- 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 +9 -1
- 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 +335 -7
- data/lib/legion/tty/screens/chat.rb +111 -24
- 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: c79bfc84ae3c3bc48ae055db72296b3aec2ab24c9a86dba5d7604e5732a3503e
|
|
4
|
+
data.tar.gz: 96cb9a58135228887222a4fcc6064209ae1e825fad949c0cbd143c40c5249ed7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea7ed87c3baffa35d8e02852ce301c532e0aef8deab17a40892db373abd7f82ac49ae60f22a88e4ea66e90f3d56996db0850cef8e8e09ffd743f8e0a924ff0e3
|
|
7
|
+
data.tar.gz: dd2e68abefc811eface7a9db9c7c2014bccf433d7b859b42068746fc952afdbcc4470ec41e761a0d86df40236bb2aebeb334afddcbe278be06fe9acb78f6715f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1] - 2026-04-18
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Alternate screen buffer** — `App#run_loop` now enables `\e[?1049h` on startup and `\e[?1049l` on shutdown so the TUI no longer pollutes shell scrollback and the prior shell session is restored on exit (closes #20)
|
|
7
|
+
- **Mouse wheel scroll** — enabled SGR mouse tracking (`\e[?1000h` + `\e[?1006h`) and routed wheel-up/wheel-down events to `MessageStream#scroll_up` / `#scroll_down` with 3-line increments (closes #20)
|
|
8
|
+
- **Vim-style scroll bindings** — added `Ctrl+B` (half-page up) and `Ctrl+F` (half-page down) on the Chat screen; wired `Home` / `End` to jump-to-top / jump-to-bottom when the input bar is empty (closes #20)
|
|
9
|
+
- **Scroll hint in status bar** — `scroll_segment` now shows directional arrows (`↑↓ scroll` or `↑ scroll`) alongside the position counter when content overflows the viewport (closes #20)
|
|
10
|
+
- **Help text updated** — added scroll binding reference line to the help overlay
|
|
11
|
+
|
|
12
|
+
## [0.5.0] - 2026-04-17
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **`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)
|
|
16
|
+
- **`/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)
|
|
17
|
+
- **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)
|
|
18
|
+
- **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)
|
|
19
|
+
- **`DaemonClient.run_tool`** — new method: `POST /api/tools/run`; returns `{status: :ok/:error/:unavailable}` (closes #15)
|
|
20
|
+
- **`/skills`** — new slash command: lists all registered `Legion::LLM::Skills` with namespace, trigger type, trigger words, and description (closes #16)
|
|
21
|
+
- **`/skills load <path>`** — loads a `.md` skill file at runtime via `Legion::LLM::Skills::DiskLoader` (closes #16)
|
|
22
|
+
- **`/skills run <namespace>:<name>`** — invokes a registered skill and shows its injected content (closes #16)
|
|
23
|
+
- **`/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)
|
|
24
|
+
- **Apollo dashboard panel** — System Info panel on the dashboard now shows `Apollo: started/transport/data` availability (closes #17)
|
|
25
|
+
- **`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)
|
|
26
|
+
- **`/gaia status`** — shows Gaia NotificationGate evaluator state: presence, arousal score, and quiet-hours status (closes #18)
|
|
27
|
+
- **`/gaia presence <status>`** — sets user presence (`Available`, `DoNotDisturb`, `Away`, etc.) for the session, controlling notification threshold (closes #18)
|
|
28
|
+
- **`/context` auto-inject skills** — `/context` output now includes auto-inject skill names when `Legion::LLM::Skills::Registry` is available (closes #16)
|
|
29
|
+
- **Debug mode skill count** — status bar debug segment shows `skills:N` when `Legion::LLM::Skills::Registry` is available (closes #16)
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `handle_tools` refactored into `handle_tools_registry` + `handle_tools_gem_scan` helpers for clarity
|
|
33
|
+
|
|
3
34
|
## [0.4.42] - 2026-04-08
|
|
4
35
|
|
|
5
36
|
### 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
|
|
@@ -22,11 +30,18 @@ module Legion
|
|
|
22
30
|
"\e[1~" => :home, "\e[4~" => :end,
|
|
23
31
|
"\x7f" => :backspace, "\b" => :backspace, "\t" => :tab,
|
|
24
32
|
"\x03" => :ctrl_c, "\x04" => :ctrl_d,
|
|
25
|
-
"\x01" => :ctrl_a, "\
|
|
33
|
+
"\x01" => :ctrl_a, "\x02" => :ctrl_b,
|
|
34
|
+
"\x05" => :ctrl_e, "\x06" => :ctrl_f,
|
|
26
35
|
"\x0B" => :ctrl_k, "\x0C" => :ctrl_l, "\x13" => :ctrl_s,
|
|
27
36
|
"\x15" => :ctrl_u
|
|
28
37
|
}.freeze
|
|
29
38
|
|
|
39
|
+
ENABLE_ALT_SCREEN = "\e[?1049h"
|
|
40
|
+
DISABLE_ALT_SCREEN = "\e[?1049l"
|
|
41
|
+
ENABLE_MOUSE = "\e[?1000h\e[?1006h"
|
|
42
|
+
DISABLE_MOUSE = "\e[?1000h\e[?1006l"
|
|
43
|
+
SGR_MOUSE_RE = /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
|
|
44
|
+
|
|
30
45
|
attr_reader :config, :credentials, :screen_manager, :hotkeys, :llm_chat, :input_bar
|
|
31
46
|
|
|
32
47
|
def self.run(argv = [])
|
|
@@ -34,7 +49,7 @@ module Legion
|
|
|
34
49
|
app = new(**opts)
|
|
35
50
|
app.start
|
|
36
51
|
rescue Interrupt => e
|
|
37
|
-
|
|
52
|
+
log.debug { "app interrupted: #{e.message}" }
|
|
38
53
|
app&.shutdown
|
|
39
54
|
end
|
|
40
55
|
|
|
@@ -70,7 +85,7 @@ module Legion
|
|
|
70
85
|
end
|
|
71
86
|
|
|
72
87
|
# Public: called by screens (e.g., Chat during LLM streaming) to force a re-render
|
|
73
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
88
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
74
89
|
def render_frame
|
|
75
90
|
width = terminal_width
|
|
76
91
|
height = terminal_height
|
|
@@ -97,9 +112,9 @@ module Legion
|
|
|
97
112
|
|
|
98
113
|
$stdout.flush
|
|
99
114
|
rescue StandardError => e
|
|
100
|
-
|
|
115
|
+
log.warn { "render_frame failed: #{e.message}" }
|
|
101
116
|
end
|
|
102
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
117
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
103
118
|
|
|
104
119
|
# Temporarily exit raw mode for blocking prompts (TTY::Prompt, etc.)
|
|
105
120
|
def with_cooked_mode(&)
|
|
@@ -149,6 +164,8 @@ module Legion
|
|
|
149
164
|
|
|
150
165
|
@running = true
|
|
151
166
|
@raw_mode = true
|
|
167
|
+
$stdout.print ENABLE_ALT_SCREEN
|
|
168
|
+
$stdout.print ENABLE_MOUSE
|
|
152
169
|
$stdout.print cursor.hide
|
|
153
170
|
$stdout.print cursor.clear_screen
|
|
154
171
|
|
|
@@ -167,9 +184,9 @@ module Legion
|
|
|
167
184
|
nil
|
|
168
185
|
ensure
|
|
169
186
|
@raw_mode = false
|
|
187
|
+
$stdout.print DISABLE_MOUSE
|
|
170
188
|
$stdout.print cursor.show
|
|
171
|
-
$stdout.print
|
|
172
|
-
$stdout.puts
|
|
189
|
+
$stdout.print DISABLE_ALT_SCREEN
|
|
173
190
|
shutdown
|
|
174
191
|
end
|
|
175
192
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -180,6 +197,9 @@ module Legion
|
|
|
180
197
|
end
|
|
181
198
|
|
|
182
199
|
def normalize_key(raw)
|
|
200
|
+
mouse = parse_sgr_mouse(raw)
|
|
201
|
+
return mouse if mouse
|
|
202
|
+
|
|
183
203
|
KEY_MAP[raw] || raw
|
|
184
204
|
end
|
|
185
205
|
|
|
@@ -205,6 +225,11 @@ module Legion
|
|
|
205
225
|
active = @screen_manager.active_screen
|
|
206
226
|
return unless active
|
|
207
227
|
|
|
228
|
+
if %i[scroll_up scroll_down].include?(key)
|
|
229
|
+
dispatch_to_screen(active, key)
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
208
233
|
if active.respond_to?(:needs_input_bar?) && active.needs_input_bar? && @input_bar
|
|
209
234
|
dispatch_to_input_screen(active, key)
|
|
210
235
|
else
|
|
@@ -289,6 +314,17 @@ module Legion
|
|
|
289
314
|
seq
|
|
290
315
|
end
|
|
291
316
|
|
|
317
|
+
def parse_sgr_mouse(raw)
|
|
318
|
+
match = SGR_MOUSE_RE.match(raw)
|
|
319
|
+
return nil unless match
|
|
320
|
+
|
|
321
|
+
button = match[1].to_i
|
|
322
|
+
return :scroll_up if button == 64
|
|
323
|
+
return :scroll_down if button == 65
|
|
324
|
+
|
|
325
|
+
nil
|
|
326
|
+
end
|
|
327
|
+
|
|
292
328
|
# --- Rendering ---
|
|
293
329
|
|
|
294
330
|
# rubocop:disable Metrics/AbcSize
|
|
@@ -316,7 +352,7 @@ module Legion
|
|
|
316
352
|
end
|
|
317
353
|
result
|
|
318
354
|
rescue StandardError => e
|
|
319
|
-
|
|
355
|
+
log.warn { "composite_overlay failed: #{e.message}" }
|
|
320
356
|
lines
|
|
321
357
|
end
|
|
322
358
|
# rubocop:enable Metrics/AbcSize
|
|
@@ -393,7 +429,7 @@ module Legion
|
|
|
393
429
|
boot_legion_subsystems
|
|
394
430
|
@llm_chat = try_settings_llm
|
|
395
431
|
rescue StandardError => e
|
|
396
|
-
|
|
432
|
+
log.warn { "setup_llm failed: #{e.message}" }
|
|
397
433
|
@llm_chat = nil
|
|
398
434
|
end
|
|
399
435
|
|
|
@@ -416,13 +452,13 @@ module Legion
|
|
|
416
452
|
File.write(identity_path, ::JSON.generate(identity))
|
|
417
453
|
@config = load_config
|
|
418
454
|
rescue StandardError => e
|
|
419
|
-
|
|
455
|
+
log.warn { "rescan_environment failed: #{e.message}" }
|
|
420
456
|
nil
|
|
421
457
|
end
|
|
422
458
|
end
|
|
423
459
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
424
460
|
|
|
425
|
-
# rubocop:disable Metrics/AbcSize, Metrics/
|
|
461
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
426
462
|
def boot_legion_subsystems
|
|
427
463
|
require 'legion/logging'
|
|
428
464
|
Legion::Logging.setup(log_level: 'error', level: 'error', trace: false)
|
|
@@ -438,10 +474,10 @@ module Legion
|
|
|
438
474
|
Legion::Crypt.start unless Legion::Crypt.instance_variable_get(:@started)
|
|
439
475
|
Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
|
|
440
476
|
rescue LoadError => e
|
|
441
|
-
|
|
477
|
+
log.debug { "legion/crypt not available: #{e.message}" }
|
|
442
478
|
nil
|
|
443
479
|
rescue StandardError => e
|
|
444
|
-
|
|
480
|
+
log.warn { "crypt/secrets setup failed: #{e.message}" }
|
|
445
481
|
nil
|
|
446
482
|
end
|
|
447
483
|
|
|
@@ -449,14 +485,14 @@ module Legion
|
|
|
449
485
|
require 'legion/llm'
|
|
450
486
|
Legion::Settings.merge_settings(:llm, Legion::LLM::Settings.default)
|
|
451
487
|
rescue LoadError => e
|
|
452
|
-
|
|
488
|
+
log.debug { "legion/llm not available: #{e.message}" }
|
|
453
489
|
nil
|
|
454
490
|
end
|
|
455
491
|
rescue LoadError => e
|
|
456
|
-
|
|
492
|
+
log.debug { "legion subsystem load failed: #{e.message}" }
|
|
457
493
|
nil
|
|
458
494
|
end
|
|
459
|
-
# rubocop:enable Metrics/AbcSize, Metrics/
|
|
495
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
460
496
|
|
|
461
497
|
def settings_search_path
|
|
462
498
|
[
|
|
@@ -471,15 +507,13 @@ module Legion
|
|
|
471
507
|
# All LLM calls route through the LegionIO daemon API.
|
|
472
508
|
# No raw RubyLLM session is created here — nil signals "use daemon path".
|
|
473
509
|
if Legion::TTY::DaemonClient.available?
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
Legion::Logging.warn('TTY: daemon not running; LLM unavailable until daemon starts')
|
|
478
|
-
end
|
|
510
|
+
log.debug { 'TTY: daemon available, LLM routed through daemon' }
|
|
511
|
+
else
|
|
512
|
+
log.warn { 'TTY: daemon not running; LLM unavailable until daemon starts' }
|
|
479
513
|
end
|
|
480
514
|
nil
|
|
481
515
|
rescue StandardError => e
|
|
482
|
-
|
|
516
|
+
log.warn { "try_settings_llm failed: #{e.message}" }
|
|
483
517
|
nil
|
|
484
518
|
end
|
|
485
519
|
|
|
@@ -549,7 +583,7 @@ module Legion
|
|
|
549
583
|
|
|
550
584
|
deep_symbolize(::JSON.parse(File.read(path)))
|
|
551
585
|
rescue ::JSON::ParserError, Errno::ENOENT => e
|
|
552
|
-
|
|
586
|
+
log.warn { "load_credentials failed: #{e.message}" }
|
|
553
587
|
{}
|
|
554
588
|
end
|
|
555
589
|
|
|
@@ -566,7 +600,7 @@ module Legion
|
|
|
566
600
|
|
|
567
601
|
deep_symbolize(::JSON.parse(File.read(path)))
|
|
568
602
|
rescue ::JSON::ParserError, Errno::ENOENT => e
|
|
569
|
-
|
|
603
|
+
log.warn { "load_config failed: #{e.message}" }
|
|
570
604
|
{}
|
|
571
605
|
end
|
|
572
606
|
|
|
@@ -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
|