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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/lib/legion/tty/app.rb +58 -24
  4. data/lib/legion/tty/background/github_probe.rb +27 -24
  5. data/lib/legion/tty/background/llm_probe.rb +12 -8
  6. data/lib/legion/tty/background/scanner.rb +19 -16
  7. data/lib/legion/tty/components/command_palette.rb +5 -1
  8. data/lib/legion/tty/components/digital_rain.rb +7 -1
  9. data/lib/legion/tty/components/input_bar.rb +12 -2
  10. data/lib/legion/tty/components/markdown_view.rb +6 -1
  11. data/lib/legion/tty/components/message_stream.rb +5 -2
  12. data/lib/legion/tty/components/model_picker.rb +5 -1
  13. data/lib/legion/tty/components/progress_panel.rb +4 -1
  14. data/lib/legion/tty/components/session_picker.rb +5 -1
  15. data/lib/legion/tty/components/status_bar.rb +9 -1
  16. data/lib/legion/tty/components/table_view.rb +7 -1
  17. data/lib/legion/tty/components/tool_call_parser.rb +4 -1
  18. data/lib/legion/tty/daemon_client.rb +16 -0
  19. data/lib/legion/tty/keybinding_manager.rb +4 -1
  20. data/lib/legion/tty/notification_gate.rb +40 -0
  21. data/lib/legion/tty/notify.rb +5 -1
  22. data/lib/legion/tty/screens/chat/export_commands.rb +8 -4
  23. data/lib/legion/tty/screens/chat/message_commands.rb +8 -4
  24. data/lib/legion/tty/screens/chat/model_commands.rb +6 -2
  25. data/lib/legion/tty/screens/chat/session_commands.rb +6 -2
  26. data/lib/legion/tty/screens/chat/ui_commands.rb +335 -7
  27. data/lib/legion/tty/screens/chat.rb +111 -24
  28. data/lib/legion/tty/screens/config.rb +6 -3
  29. data/lib/legion/tty/screens/dashboard.rb +26 -9
  30. data/lib/legion/tty/screens/extensions.rb +4 -1
  31. data/lib/legion/tty/screens/onboarding.rb +79 -75
  32. data/lib/legion/tty/session_store.rb +6 -3
  33. data/lib/legion/tty/version.rb +1 -1
  34. data/lib/legion/tty.rb +1 -0
  35. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 544115343036952adfc2f3accc8ce189ec8bb13c06f04ce383d0536549118e49
4
- data.tar.gz: 806f22df6bed2c34ce6c47c6eab8c1af6bb611cc458db878087a86163f7e0248
3
+ metadata.gz: c79bfc84ae3c3bc48ae055db72296b3aec2ab24c9a86dba5d7604e5732a3503e
4
+ data.tar.gz: 96cb9a58135228887222a4fcc6064209ae1e825fad949c0cbd143c40c5249ed7
5
5
  SHA512:
6
- metadata.gz: 4a90450fe8ba541b1823f43cfc89eb97ed2ef6185029afffc022771c87f8909583fab80715098789240b8bab4ee3c8c2ae240987dc9630352c76d89ee3acad6a
7
- data.tar.gz: '0667478fd7867da23c0c5f4a83fdeac3056cc97ff18a2bf1dbe0c447f4a29088b0e1d6aafeb6d9b19ffc8a1784b4cd80f1a94a2d9ea5a2563fca063004020ee5'
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
@@ -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, "\x05" => :ctrl_e,
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
- Legion::Logging.debug("app interrupted: #{e.message}") if defined?(Legion::Logging)
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, Metrics/PerceivedComplexity
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
- Legion::Logging.warn("render_frame failed: #{e.message}") if defined?(Legion::Logging)
115
+ log.warn { "render_frame failed: #{e.message}" }
101
116
  end
102
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
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 cursor.move_to(0, terminal_height - 1)
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
- Legion::Logging.warn("composite_overlay failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("setup_llm failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("rescan_environment failed: #{e.message}") if defined?(Legion::Logging)
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/CyclomaticComplexity, Metrics/MethodLength
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
- Legion::Logging.debug("legion/crypt not available: #{e.message}") if defined?(Legion::Logging)
477
+ log.debug { "legion/crypt not available: #{e.message}" }
442
478
  nil
443
479
  rescue StandardError => e
444
- Legion::Logging.warn("crypt/secrets setup failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("legion/llm not available: #{e.message}") if defined?(Legion::Logging)
488
+ log.debug { "legion/llm not available: #{e.message}" }
453
489
  nil
454
490
  end
455
491
  rescue LoadError => e
456
- Legion::Logging.debug("legion subsystem load failed: #{e.message}") if defined?(Legion::Logging)
492
+ log.debug { "legion subsystem load failed: #{e.message}" }
457
493
  nil
458
494
  end
459
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
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
- Legion::Logging.debug('TTY: daemon available, LLM routed through daemon') if defined?(Legion::Logging)
475
- elsif defined?(Legion::Logging)
476
- if defined?(Legion::Logging)
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
- Legion::Logging.warn("try_settings_llm failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("load_credentials failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("load_config failed: #{e.message}") if defined?(Legion::Logging)
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
- @log = logger
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
- @log&.log('github', 'quick probe skipped (no token)')
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
- @log&.log('github', 'quick probe: fetching /user + recent commits')
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
- @log&.log('github', "quick probe complete in #{elapsed}ms")
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
- @log&.log('github', "quick probe ERROR: #{e.class}: #{e.message}")
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
- @log&.log('github', "probing with #{remotes.size} remotes: #{remotes.first(5).inspect}")
46
- @log&.log('github', "token: #{@token ? 'present' : 'NONE'}")
47
- @log&.log('github', "quick_profile: #{quick_profile ? 'reusing' : 'none'}")
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
- @log&.log('github', "probe complete in #{elapsed}ms")
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
- @log&.log('github', "ERROR: #{e.class}: #{e.message}")
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
- @log&.log('github', "quick: authenticated as #{username}")
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
- @log&.log('github', "quick: commits this week=#{commits_week} this month=#{commits_month}")
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
- Legion::Logging.debug("count_commits failed: #{e.message}") if defined?(Legion::Logging)
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
- @log&.log('github', "reusing quick profile for: #{username}")
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
- @log&.log('github', 'authenticated /user failed, falling back to public')
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
- @log&.log('github', "authenticated as: #{username}")
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
- @log&.log('github', "inferred username: #{username || 'none'}")
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
- @log&.log('github', 'token source: environment variable')
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
- @log&.log('github', 'token source: gh CLI')
300
+ @boot_log&.log('github', 'token source: gh CLI')
298
301
  return gh_token
299
302
  end
300
303
 
301
- @log&.log('github', 'no token found (no env var, no gh CLI)')
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
- @log&.log('github', "found gh CLI at #{gh_path}")
312
+ @boot_log&.log('github', "found gh CLI at #{gh_path}")
310
313
 
311
314
  status = `gh auth status 2>&1`
312
- @log&.log('github', "gh auth status: #{status.lines.first&.strip}")
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
- @log&.log('github', "gh CLI error: #{e.message}")
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
- Legion::Logging.debug("api_get failed: #{e.message}") if defined?(Legion::Logging)
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
- @log = logger
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
- @log&.log('llm_probe', "error: #{e.message}")
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
- @log&.log('llm_probe', 'bootstrap wait timed out')
43
+ @boot_log&.log('llm_probe', 'bootstrap wait timed out')
40
44
  else
41
- @log&.log('llm_probe', 'bootstrap wait complete')
45
+ @boot_log&.log('llm_probe', 'bootstrap wait complete')
42
46
  end
43
47
  rescue StandardError => e
44
- @log&.log('llm_probe', "bootstrap wait error: #{e.message}")
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
- @log&.log('llm_probe', "LLM start failed: #{e.message}")
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
- @log&.log('llm_probe', "#{name}: #{result[:status]} (#{result[:latency_ms]}ms)")
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
- Legion::Logging.debug("ping_provider #{name} failed: #{e.message}") if defined?(Legion::Logging)
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
- @log = logger
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
- @log&.log('scanner', "starting scan of #{@base_dirs.join(', ')}")
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
- @log&.log('scanner', "scan complete in #{elapsed}ms")
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
- @log&.log('scanner', "ERROR: #{e.class}: #{e.message}")
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
- Legion::Logging.debug("port_open? #{host}:#{port} failed: #{e.message}") if defined?(Legion::Logging)
96
+ log.debug { "port_open? #{host}:#{port} failed: #{e.message}" }
94
97
  false
95
98
  end
96
99
 
97
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
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
- Legion::Logging.debug("collect_repos child failed: #{e.message}") if defined?(Legion::Logging)
112
+ log.debug { "collect_repos child failed: #{e.message}" }
110
113
  next
111
114
  end
112
115
  rescue StandardError => e
113
- Legion::Logging.debug("collect_repos failed: #{e.message}") if defined?(Legion::Logging)
116
+ log.debug { "collect_repos failed: #{e.message}" }
114
117
  []
115
118
  end
116
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
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
- Legion::Logging.debug("git_remote failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("git_branch failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("read_history_lines failed for #{path}: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("scan_gitconfig failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("scan_jfrog failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("scan_terraform failed: #{e.message}") if defined?(Legion::Logging)
216
+ log.debug { "scan_terraform failed: #{e.message}" }
214
217
  nil
215
218
  end
216
219
  end