legion-tty 0.4.41 → 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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +3 -3
  4. data/lib/legion/tty/app.rb +27 -21
  5. data/lib/legion/tty/background/github_probe.rb +27 -24
  6. data/lib/legion/tty/background/kerberos_probe.rb +2 -1
  7. data/lib/legion/tty/background/llm_probe.rb +12 -8
  8. data/lib/legion/tty/background/scanner.rb +19 -16
  9. data/lib/legion/tty/components/command_palette.rb +5 -1
  10. data/lib/legion/tty/components/digital_rain.rb +7 -1
  11. data/lib/legion/tty/components/markdown_view.rb +6 -1
  12. data/lib/legion/tty/components/message_stream.rb +5 -2
  13. data/lib/legion/tty/components/model_picker.rb +5 -1
  14. data/lib/legion/tty/components/progress_panel.rb +4 -1
  15. data/lib/legion/tty/components/session_picker.rb +5 -1
  16. data/lib/legion/tty/components/status_bar.rb +7 -0
  17. data/lib/legion/tty/components/table_view.rb +7 -1
  18. data/lib/legion/tty/components/tool_call_parser.rb +4 -1
  19. data/lib/legion/tty/daemon_client.rb +46 -13
  20. data/lib/legion/tty/keybinding_manager.rb +4 -1
  21. data/lib/legion/tty/notification_gate.rb +40 -0
  22. data/lib/legion/tty/notify.rb +5 -1
  23. data/lib/legion/tty/screens/chat/export_commands.rb +8 -4
  24. data/lib/legion/tty/screens/chat/message_commands.rb +8 -4
  25. data/lib/legion/tty/screens/chat/model_commands.rb +6 -2
  26. data/lib/legion/tty/screens/chat/session_commands.rb +6 -2
  27. data/lib/legion/tty/screens/chat/ui_commands.rb +333 -6
  28. data/lib/legion/tty/screens/chat.rb +90 -16
  29. data/lib/legion/tty/screens/config.rb +6 -3
  30. data/lib/legion/tty/screens/dashboard.rb +26 -9
  31. data/lib/legion/tty/screens/extensions.rb +4 -1
  32. data/lib/legion/tty/screens/onboarding.rb +79 -75
  33. data/lib/legion/tty/session_store.rb +6 -3
  34. data/lib/legion/tty/version.rb +1 -1
  35. data/lib/legion/tty.rb +1 -0
  36. metadata +4 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e5ecb9de86922a5a3d4dd3b3eca6b4f7864103c89399a8a945bbae95e749814
4
- data.tar.gz: 0d25f75d79303fd7a42a9fbf125768503c48f16e5c749b799cd2013d68bae99a
3
+ metadata.gz: 20065c242e16ca4262309393cdad733b473f139c1efd288512ffe9d704fb5b69
4
+ data.tar.gz: 1159cc15d4545b6dbda4d9a69087a9f9a929c6579fd477c1ae0a27d12dbf95c5
5
5
  SHA512:
6
- metadata.gz: 804bc560650b61a6987a8f9d4d02cd7a315caeaccd1823f1b11a4e1b1fe024fc0eb37a35c59bafe654c0738f2e201af9b04d7274bd9c7d8df02b563c6551336b
7
- data.tar.gz: 15ab28737a1677ae28285400d1e9ae6b794f1ea021e4cc7c194e949e59a97df56c054419d21fd351c70ea97636608d09a8a6737ca7a690e17b398e87531e6c1c
6
+ metadata.gz: 77d12618eb36e18a62e3c0fbfa973f645155a4611771982d596b03803a1c98e3e4b592f3b40699842cd23f977ecb86518c1cdd3a26a4e58db936f0f45ba6137c
7
+ data.tar.gz: 4b8f2551be051f7140c851f9cac8a632430a102eda8f0ca78a5b1750fb0f2c266fc903fafd6e01606c5a2222986ab1cab3539003d41120bfac8a2762957de329
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
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
+
25
+ ## [0.4.42] - 2026-04-08
26
+
27
+ ### Changed
28
+ - `DaemonClient` now uses `Legion::Logging::Helper` (structured logging via `log.info`/`log.debug`) instead of inline `Legion::Logging.method if defined?` guards for all logging and exception handling
29
+ - Bumped `legion-logging` minimum dependency from `>= 1.2.8` to `>= 1.5.0` to use `handle_exception` from `Legion::Logging::Helper`
30
+ - Extracted `store_manifest` and `parse_inference_response` private helpers to reduce method complexity in `fetch_manifest` and `inference`
31
+
32
+ ### Fixed
33
+ - `KerberosProbe#days_in_month` used `Time.new(year, month, -1)` which raises `ArgumentError` for day `-1`; replaced with `Date.new(year, month, -1).day` (correct Ruby idiom for last day of month)
34
+
3
35
  ## [0.4.41] - 2026-03-31
4
36
 
5
37
  ### Added
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Rich terminal UI for the LegionIO async cognition engine.
4
4
 
5
- **Version**: 0.4.35
5
+ **Version**: 0.4.42
6
6
 
7
7
  Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell with 115 slash commands, operational dashboard, extensions browser, config editor, and session persistence - all rendered with the [tty-ruby](https://ttytoolkit.org/) gem ecosystem.
8
8
 
@@ -289,8 +289,8 @@ Boot logs go to `~/.legionio/logs/tty-boot.log`.
289
289
 
290
290
  ```bash
291
291
  bundle install
292
- bundle exec rspec # 1817 examples, 0 failures
293
- bundle exec rubocop # 150 files, 0 offenses
292
+ bundle exec rspec # 1952 examples, 0 failures
293
+ bundle exec rubocop # 163 files, 0 offenses
294
294
  ```
295
295
 
296
296
  ## License
@@ -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
- Legion::Logging.debug("app interrupted: #{e.message}") if defined?(Legion::Logging)
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, Metrics/PerceivedComplexity
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
- Legion::Logging.warn("render_frame failed: #{e.message}") if defined?(Legion::Logging)
108
+ log.warn { "render_frame failed: #{e.message}" }
101
109
  end
102
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
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
- Legion::Logging.warn("composite_overlay failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("setup_llm failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("rescan_environment failed: #{e.message}") if defined?(Legion::Logging)
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/CyclomaticComplexity, Metrics/MethodLength
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
- Legion::Logging.debug("legion/crypt not available: #{e.message}") if defined?(Legion::Logging)
449
+ log.debug { "legion/crypt not available: #{e.message}" }
442
450
  nil
443
451
  rescue StandardError => e
444
- Legion::Logging.warn("crypt/secrets setup failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.debug("legion/llm not available: #{e.message}") if defined?(Legion::Logging)
460
+ log.debug { "legion/llm not available: #{e.message}" }
453
461
  nil
454
462
  end
455
463
  rescue LoadError => e
456
- Legion::Logging.debug("legion subsystem load failed: #{e.message}") if defined?(Legion::Logging)
464
+ log.debug { "legion subsystem load failed: #{e.message}" }
457
465
  nil
458
466
  end
459
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
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
- 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
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
- Legion::Logging.warn("try_settings_llm failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("load_credentials failed: #{e.message}") if defined?(Legion::Logging)
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
- Legion::Logging.warn("load_config failed: #{e.message}") if defined?(Legion::Logging)
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
- @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,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
3
4
  require 'resolv'
4
5
  require 'shellwords'
5
6
 
@@ -185,7 +186,7 @@ module Legion
185
186
  # rubocop:enable Metrics/AbcSize
186
187
 
187
188
  def days_in_month(month, year)
188
- Time.new(year, month, -1).day
189
+ Date.new(year, month, -1).day
189
190
  end
190
191
 
191
192
  def log_result(result, elapsed)
@@ -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
@@ -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
- Legion::Logging.debug("command palette cancelled: #{e.message}") if defined?(Legion::Logging)
43
+ log.debug { "command palette cancelled: #{e.message}" }
40
44
  nil
41
45
  end
42
46
  end