legion-tty 0.4.28 → 0.4.30

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4f86c43e057ed6d85f49e98af69b9c48bb4256751ee93ad6ca46056fce5e9a7
4
- data.tar.gz: b474e1c01bf675a7833cdc4b1cebacef605376ed4e4ddbc61d621456c525b7cc
3
+ metadata.gz: f37085f25b964517a88988f6780ae8d60257356d53d3021567a4f9f0bc43f400
4
+ data.tar.gz: d1b150a3993d20e92eb30d778bbd4fe79c75582ec3ad893668879d5abc70ae76
5
5
  SHA512:
6
- metadata.gz: 6bf396cf31f2c03c3db4b7569ed1d6c79b26506bb2316a0e05111538b35c8005d40104cdd9cb31937ed5046fe29c5f4e14b4cac33eddb897f7681e70187df132
7
- data.tar.gz: 3f1d3a31eb2e636d4b6df7e990ba03f8f690ef31ab4bb2d1ee153e597d94698bded853e31b779a62e56eb88bae9a5423d8832f66577be0a7a053cb4a36507829
6
+ metadata.gz: 810a96e9a0f5a0487e3e39c9d895166f7e04810d63e27ba8ed1761520720c2cdd48755784ebbcedeb88d4e26e4686297ea4ef92777465689ed89423653f07f41
7
+ data.tar.gz: 4be0e4271d6a04f430be490e54952a013f9dce0e5269f9a1211a1437d9124dbd4401887150f0a105b949a3d26f82471e3da5faf69c0384950e2b3cdd690e3c6a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.30] - 2026-03-22
4
+
5
+ ### Fixed
6
+ - Add `Legion::Logging.debug/warn` to all previously silent rescue blocks across lib/ files
7
+ - Add rubocop disable/enable directives for Metrics cops on methods whose complexity increased from logging additions
8
+
9
+ ## [0.4.29] - 2026-03-20
10
+
11
+ ### Fixed
12
+ - RuboCop `Style/SingleArgumentDig` offense in onboarding teams detection
13
+ - RuboCop `Layout/LineLength` offense in onboarding teams spec
14
+
3
15
  ## [0.4.28] - 2026-03-19
4
16
 
5
17
  ### 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.27
5
+ **Version**: 0.4.28
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
 
@@ -19,7 +19,8 @@ module Legion
19
19
  opts = parse_argv(argv)
20
20
  app = new(**opts)
21
21
  app.start
22
- rescue Interrupt
22
+ rescue Interrupt => e
23
+ Legion::Logging.debug("app interrupted: #{e.message}") if defined?(Legion::Logging)
23
24
  app&.shutdown
24
25
  end
25
26
 
@@ -91,7 +92,8 @@ module Legion
91
92
  def setup_llm
92
93
  boot_legion_subsystems
93
94
  @llm_chat = try_settings_llm
94
- rescue StandardError
95
+ rescue StandardError => e
96
+ Legion::Logging.warn("setup_llm failed: #{e.message}") if defined?(Legion::Logging)
95
97
  @llm_chat = nil
96
98
  end
97
99
 
@@ -113,7 +115,8 @@ module Legion
113
115
  }.compact
114
116
  File.write(identity_path, ::JSON.generate(identity))
115
117
  @config = load_config
116
- rescue StandardError
118
+ rescue StandardError => e
119
+ Legion::Logging.warn("rescan_environment failed: #{e.message}") if defined?(Legion::Logging)
117
120
  nil
118
121
  end
119
122
  end
@@ -132,6 +135,7 @@ module Legion
132
135
 
133
136
  private
134
137
 
138
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
135
139
  def boot_legion_subsystems
136
140
  # Follow the same init order as Legion::Service:
137
141
  # 1. logging 2. settings 3. crypt 4. resolve secrets 5. LLM merge
@@ -148,19 +152,26 @@ module Legion
148
152
  require 'legion/crypt'
149
153
  Legion::Crypt.start unless Legion::Crypt.instance_variable_get(:@started)
150
154
  Legion::Settings.resolve_secrets! if Legion::Settings.respond_to?(:resolve_secrets!)
151
- rescue LoadError, StandardError
155
+ rescue LoadError => e
156
+ Legion::Logging.debug("legion/crypt not available: #{e.message}") if defined?(Legion::Logging)
157
+ nil
158
+ rescue StandardError => e
159
+ Legion::Logging.warn("crypt/secrets setup failed: #{e.message}") if defined?(Legion::Logging)
152
160
  nil
153
161
  end
154
162
 
155
163
  begin
156
164
  require 'legion/llm'
157
165
  Legion::Settings.merge_settings(:llm, Legion::LLM::Settings.default)
158
- rescue LoadError
166
+ rescue LoadError => e
167
+ Legion::Logging.debug("legion/llm not available: #{e.message}") if defined?(Legion::Logging)
159
168
  nil
160
169
  end
161
- rescue LoadError
170
+ rescue LoadError => e
171
+ Legion::Logging.debug("legion subsystem load failed: #{e.message}") if defined?(Legion::Logging)
162
172
  nil
163
173
  end
174
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
164
175
 
165
176
  def settings_search_path
166
177
  [
@@ -181,7 +192,8 @@ module Legion
181
192
  return nil unless provider
182
193
 
183
194
  Legion::LLM.chat(provider: provider)
184
- rescue StandardError
195
+ rescue StandardError => e
196
+ Legion::Logging.warn("try_settings_llm failed: #{e.message}") if defined?(Legion::Logging)
185
197
  nil
186
198
  end
187
199
 
@@ -246,7 +258,8 @@ module Legion
246
258
  return {} unless File.exist?(path)
247
259
 
248
260
  deep_symbolize(::JSON.parse(File.read(path)))
249
- rescue ::JSON::ParserError, Errno::ENOENT
261
+ rescue ::JSON::ParserError, Errno::ENOENT => e
262
+ Legion::Logging.warn("load_credentials failed: #{e.message}") if defined?(Legion::Logging)
250
263
  {}
251
264
  end
252
265
 
@@ -262,7 +275,8 @@ module Legion
262
275
  return {} unless File.exist?(path)
263
276
 
264
277
  deep_symbolize(::JSON.parse(File.read(path)))
265
- rescue ::JSON::ParserError, Errno::ENOENT
278
+ rescue ::JSON::ParserError, Errno::ENOENT => e
279
+ Legion::Logging.warn("load_config failed: #{e.message}") if defined?(Legion::Logging)
266
280
  {}
267
281
  end
268
282
 
@@ -104,7 +104,8 @@ module Legion
104
104
  return 0 unless data.is_a?(Hash)
105
105
 
106
106
  data['total_count'] || 0
107
- rescue StandardError
107
+ rescue StandardError => e
108
+ Legion::Logging.debug("count_commits failed: #{e.message}") if defined?(Legion::Logging)
108
109
  0
109
110
  end
110
111
 
@@ -326,7 +327,8 @@ module Legion
326
327
  http = build_http(uri)
327
328
  response = http.request(build_request(uri))
328
329
  ::JSON.parse(response.body)
329
- rescue StandardError
330
+ rescue StandardError => e
331
+ Legion::Logging.debug("api_get failed: #{e.message}") if defined?(Legion::Logging)
330
332
  nil
331
333
  end
332
334
 
@@ -53,6 +53,7 @@ module Legion
53
53
  { name: name, model: model, status: :ok, latency_ms: latency }
54
54
  rescue StandardError => e
55
55
  latency = ((Time.now - start_time) * 1000).round
56
+ Legion::Logging.debug("ping_provider #{name} failed: #{e.message}") if defined?(Legion::Logging)
56
57
  { name: name, model: model, status: :error, latency_ms: latency, error: e.message }
57
58
  end
58
59
  end
@@ -89,10 +89,12 @@ module Legion
89
89
 
90
90
  def port_open?(host, port)
91
91
  ::Socket.tcp(host, port, connect_timeout: 1) { true }
92
- rescue StandardError
92
+ rescue StandardError => e
93
+ Legion::Logging.debug("port_open? #{host}:#{port} failed: #{e.message}") if defined?(Legion::Logging)
93
94
  false
94
95
  end
95
96
 
97
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
96
98
  def collect_repos(base, depth = 0)
97
99
  return [] unless File.directory?(base)
98
100
  return [build_repo_entry(base)] if File.directory?(File.join(base, '.git'))
@@ -103,12 +105,15 @@ module Legion
103
105
 
104
106
  child_path = File.join(base, child)
105
107
  acc.concat(collect_repos(child_path, depth + 1)) if File.directory?(child_path)
106
- rescue StandardError
108
+ rescue StandardError => e
109
+ Legion::Logging.debug("collect_repos child failed: #{e.message}") if defined?(Legion::Logging)
107
110
  next
108
111
  end
109
- rescue StandardError
112
+ rescue StandardError => e
113
+ Legion::Logging.debug("collect_repos failed: #{e.message}") if defined?(Legion::Logging)
110
114
  []
111
115
  end
116
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
112
117
 
113
118
  def build_repo_entry(path)
114
119
  { path: path, name: File.basename(path), remote: git_remote(path),
@@ -118,14 +123,16 @@ module Legion
118
123
  def git_remote(path)
119
124
  out = `git -C #{path.shellescape} remote get-url origin 2>/dev/null`.strip
120
125
  out.empty? ? nil : out
121
- rescue StandardError
126
+ rescue StandardError => e
127
+ Legion::Logging.debug("git_remote failed: #{e.message}") if defined?(Legion::Logging)
122
128
  nil
123
129
  end
124
130
 
125
131
  def git_branch(path)
126
132
  out = `git -C #{path.shellescape} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
127
133
  out.empty? ? nil : out
128
- rescue StandardError
134
+ rescue StandardError => e
135
+ Legion::Logging.debug("git_branch failed: #{e.message}") if defined?(Legion::Logging)
129
136
  nil
130
137
  end
131
138
 
@@ -144,7 +151,8 @@ module Legion
144
151
  next [] unless File.exist?(full)
145
152
 
146
153
  File.readlines(full, encoding: 'utf-8', chomp: true).last(500)
147
- rescue StandardError
154
+ rescue StandardError => e
155
+ Legion::Logging.debug("read_history_lines failed for #{path}: #{e.message}") if defined?(Legion::Logging)
148
156
  []
149
157
  end
150
158
  end
@@ -169,7 +177,8 @@ module Legion
169
177
  result = { name: name.empty? ? nil : name, email: email.empty? ? nil : email }
170
178
  result[:signing_key] = signing_key unless signing_key.empty?
171
179
  result
172
- rescue StandardError
180
+ rescue StandardError => e
181
+ Legion::Logging.debug("scan_gitconfig failed: #{e.message}") if defined?(Legion::Logging)
173
182
  nil
174
183
  end
175
184
 
@@ -185,7 +194,8 @@ module Legion
185
194
  servers.map do |s|
186
195
  { server_id: s[:serverId], url: s[:url], user: s[:user] }
187
196
  end
188
- rescue StandardError
197
+ rescue StandardError => e
198
+ Legion::Logging.debug("scan_jfrog failed: #{e.message}") if defined?(Legion::Logging)
189
199
  nil
190
200
  end
191
201
 
@@ -199,7 +209,8 @@ module Legion
199
209
  return nil if hosts.empty?
200
210
 
201
211
  { hosts: hosts.map(&:to_s) }
202
- rescue StandardError
212
+ rescue StandardError => e
213
+ Legion::Logging.debug("scan_terraform failed: #{e.message}") if defined?(Legion::Logging)
203
214
  nil
204
215
  end
205
216
  end
@@ -35,7 +35,8 @@ module Legion
35
35
  prompt = ::TTY::Prompt.new(output: output)
36
36
  choices = entries.map { |e| { name: "#{e[:label]} (#{e[:category]})", value: e[:label] } }
37
37
  prompt.select('Command:', choices, filter: true, per_page: 15)
38
- rescue ::TTY::Reader::InputInterrupt, Interrupt
38
+ rescue ::TTY::Reader::InputInterrupt, Interrupt => e
39
+ Legion::Logging.debug("command palette cancelled: #{e.message}") if defined?(Legion::Logging)
39
40
  nil
40
41
  end
41
42
  end
@@ -39,7 +39,8 @@ module Legion
39
39
  gems = Gem::Specification.select { |s| s.name.start_with?('lex-') }
40
40
  .map { |s| s.name.sub(/^lex-/, '') }
41
41
  gems.empty? ? FALLBACK_NAMES : gems
42
- rescue StandardError
42
+ rescue StandardError => e
43
+ Legion::Logging.debug("extension_names failed: #{e.message}") if defined?(Legion::Logging)
43
44
  FALLBACK_NAMES
44
45
  end
45
46
 
@@ -54,7 +54,8 @@ module Legion
54
54
  reader = ::TTY::Reader.new(history_cycle: true)
55
55
  register_tab_completion(reader)
56
56
  reader
57
- rescue LoadError
57
+ rescue LoadError => e
58
+ Legion::Logging.debug("tty-reader not available: #{e.message}") if defined?(Legion::Logging)
58
59
  nil
59
60
  end
60
61
 
@@ -9,6 +9,7 @@ module Legion
9
9
  def self.render(text, width: 80)
10
10
  ::TTY::Markdown.parse(text, width: width)
11
11
  rescue StandardError => e
12
+ Legion::Logging.warn("markdown render failed: #{e.message}") if defined?(Legion::Logging)
12
13
  "#{text}\n(markdown render error: #{e.message})"
13
14
  end
14
15
  end
@@ -182,7 +182,8 @@ module Legion
182
182
  def render_markdown(text, width)
183
183
  require_relative 'markdown_view'
184
184
  MarkdownView.render(text, width: width)
185
- rescue StandardError
185
+ rescue StandardError => e
186
+ Legion::Logging.warn("render_markdown failed: #{e.message}") if defined?(Legion::Logging)
186
187
  text
187
188
  end
188
189
 
@@ -206,7 +207,8 @@ module Legion
206
207
  @highlights.reduce(text) do |result, pattern|
207
208
  result.gsub(pattern) { "#{HIGHLIGHT_COLOR}#{$LAST_MATCH_INFO}#{HIGHLIGHT_RESET}" }
208
209
  end
209
- rescue StandardError
210
+ rescue StandardError => e
211
+ Legion::Logging.warn("apply_highlights failed: #{e.message}") if defined?(Legion::Logging)
210
212
  text
211
213
  end
212
214
 
@@ -37,7 +37,8 @@ module Legion
37
37
  { name: "#{m[:provider]} / #{m[:model]}#{indicator}", value: m }
38
38
  end
39
39
  prompt.select('Select model:', choices, per_page: 10)
40
- rescue ::TTY::Reader::InputInterrupt, Interrupt
40
+ rescue ::TTY::Reader::InputInterrupt, Interrupt => e
41
+ Legion::Logging.debug("model picker cancelled: #{e.message}") if defined?(Legion::Logging)
41
42
  nil
42
43
  end
43
44
  end
@@ -61,7 +61,8 @@ module Legion
61
61
  output: @output,
62
62
  width: 40
63
63
  )
64
- rescue LoadError
64
+ rescue LoadError => e
65
+ Legion::Logging.debug("tty-progressbar not available: #{e.message}") if defined?(Legion::Logging)
65
66
  nil
66
67
  end
67
68
  end
@@ -19,7 +19,8 @@ module Legion
19
19
  end
20
20
  choices << { name: '+ New session', value: :new }
21
21
  prompt.select('Select session:', choices, per_page: 10)
22
- rescue ::TTY::Reader::InputInterrupt, Interrupt
22
+ rescue ::TTY::Reader::InputInterrupt, Interrupt => e
23
+ Legion::Logging.debug("session picker cancelled: #{e.message}") if defined?(Legion::Logging)
23
24
  nil
24
25
  end
25
26
  end
@@ -9,6 +9,7 @@ module Legion
9
9
  table = ::TTY::Table.new(header: headers, rows: rows)
10
10
  table.render(:unicode, width: width, padding: [0, 1]) || ''
11
11
  rescue StandardError => e
12
+ Legion::Logging.warn("table render failed: #{e.message}") if defined?(Legion::Logging)
12
13
  "Table render error: #{e.message}"
13
14
  end
14
15
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/json'
4
+
5
+ module Legion
6
+ module TTY
7
+ module Components
8
+ class ToolCallParser
9
+ OPEN_TAG = '<tool_call>'
10
+ CLOSE_TAG = '</tool_call>'
11
+ MAX_BUFFER = 4096
12
+
13
+ def initialize(on_text:, on_tool_call:)
14
+ @on_text = on_text
15
+ @on_tool_call = on_tool_call
16
+ reset
17
+ end
18
+
19
+ def feed(text)
20
+ @working = @buffer + text
21
+ @buffer = +''
22
+
23
+ loop do
24
+ break if @working.empty?
25
+
26
+ if @state == :passthrough
27
+ break unless advance_passthrough?
28
+ else
29
+ break unless advance_buffering?
30
+ end
31
+ end
32
+
33
+ @buffer = @working
34
+ end
35
+
36
+ def flush
37
+ return if @buffer.empty?
38
+
39
+ @on_text.call(@buffer)
40
+ @buffer = +''
41
+ @state = :passthrough
42
+ end
43
+
44
+ def reset
45
+ @buffer = +''
46
+ @working = +''
47
+ @state = :passthrough
48
+ end
49
+
50
+ private
51
+
52
+ def advance_passthrough?
53
+ idx = @working.index(OPEN_TAG)
54
+
55
+ if idx
56
+ @on_text.call(@working[0...idx]) unless idx.zero?
57
+ @working = @working[idx..]
58
+ @state = :buffering
59
+ true
60
+ elsif partial_open_match?
61
+ false
62
+ else
63
+ @on_text.call(@working)
64
+ @working = +''
65
+ false
66
+ end
67
+ end
68
+
69
+ def advance_buffering?
70
+ idx = @working.index(CLOSE_TAG)
71
+
72
+ if idx
73
+ end_pos = idx + CLOSE_TAG.length
74
+ emit_tool_call(@working[OPEN_TAG.length...idx])
75
+ @working = @working[end_pos..]
76
+ @state = :passthrough
77
+ true
78
+ elsif @working.length > MAX_BUFFER
79
+ @on_text.call(@working)
80
+ @working = +''
81
+ @state = :passthrough
82
+ false
83
+ else
84
+ false
85
+ end
86
+ end
87
+
88
+ def emit_tool_call(json_str)
89
+ parsed = Legion::JSON.load(json_str.strip)
90
+ name = parsed[:name] || parsed['name']
91
+ args = parsed[:arguments] || parsed['arguments'] || {}
92
+
93
+ unless name
94
+ @on_text.call("#{OPEN_TAG}#{json_str}#{CLOSE_TAG}")
95
+ return
96
+ end
97
+
98
+ @on_tool_call.call(name: name, args: args)
99
+ rescue StandardError => e
100
+ Legion::Logging.warn("emit_tool_call failed: #{e.message}") if defined?(Legion::Logging)
101
+ @on_text.call("#{OPEN_TAG}#{json_str}#{CLOSE_TAG}")
102
+ end
103
+
104
+ def partial_open_match?
105
+ tag = OPEN_TAG
106
+ (1...([tag.length, @working.length].min + 1)).any? do |len|
107
+ suffix = @working[-len..]
108
+ tag.start_with?(suffix) && suffix.length < tag.length
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  require 'net/http'
4
4
  require 'uri'
5
- require 'json'
6
5
  require 'fileutils'
6
+ require 'legion/json'
7
7
 
8
8
  module Legion
9
9
  module TTY
@@ -24,7 +24,8 @@ module Legion
24
24
  http.get(uri.path)
25
25
  end
26
26
  response.code.to_i == 200
27
- rescue StandardError
27
+ rescue StandardError => e
28
+ Legion::Logging.debug("daemon available? check failed: #{e.message}") if defined?(Legion::Logging)
28
29
  false
29
30
  end
30
31
 
@@ -35,11 +36,12 @@ module Legion
35
36
  end
36
37
  return nil unless response.code.to_i == 200
37
38
 
38
- body = ::JSON.parse(response.body, symbolize_names: true)
39
+ body = Legion::JSON.load(response.body)
39
40
  @manifest = body[:data]
40
41
  write_cache(@manifest)
41
42
  @manifest
42
- rescue StandardError
43
+ rescue StandardError => e
44
+ Legion::Logging.warn("fetch_manifest failed: #{e.message}") if defined?(Legion::Logging)
43
45
  nil
44
46
  end
45
47
 
@@ -48,8 +50,9 @@ module Legion
48
50
 
49
51
  return nil unless @cache_file && File.exist?(@cache_file)
50
52
 
51
- @manifest = ::JSON.parse(File.read(@cache_file), symbolize_names: true)
52
- rescue StandardError
53
+ @manifest = Legion::JSON.load(File.read(@cache_file))
54
+ rescue StandardError => e
55
+ Legion::Logging.warn("cached_manifest failed: #{e.message}") if defined?(Legion::Logging)
53
56
  nil
54
57
  end
55
58
 
@@ -75,13 +78,14 @@ module Legion
75
78
  return nil unless available?
76
79
 
77
80
  uri = URI("#{daemon_url}/api/llm/chat")
78
- payload = ::JSON.dump({ message: message, model: model, provider: provider })
81
+ payload = Legion::JSON.dump({ message: message, model: model, provider: provider })
79
82
  response = post_json(uri, payload)
80
83
 
81
84
  return nil unless response && SUCCESS_CODES.include?(response.code.to_i)
82
85
 
83
- ::JSON.parse(response.body, symbolize_names: true)
84
- rescue StandardError
86
+ Legion::JSON.load(response.body)
87
+ rescue StandardError => e
88
+ Legion::Logging.warn("chat failed: #{e.message}") if defined?(Legion::Logging)
85
89
  nil
86
90
  end
87
91
 
@@ -109,8 +113,9 @@ module Legion
109
113
  return unless @cache_file
110
114
 
111
115
  FileUtils.mkdir_p(File.dirname(@cache_file))
112
- File.write(@cache_file, ::JSON.dump(data))
113
- rescue StandardError
116
+ File.write(@cache_file, Legion::JSON.dump(data))
117
+ rescue StandardError => e
118
+ Legion::Logging.warn("write_cache failed: #{e.message}") if defined?(Legion::Logging)
114
119
  nil
115
120
  end
116
121
  end
@@ -15,6 +15,7 @@ module Legion
15
15
  @message_stream.add_message(role: :system, content: "Exported to: #{path}")
16
16
  :handled
17
17
  rescue StandardError => e
18
+ Legion::Logging.warn("handle_export failed: #{e.message}") if defined?(Legion::Logging)
18
19
  @message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
19
20
  :handled
20
21
  end
@@ -128,6 +129,7 @@ module Legion
128
129
  @message_stream.add_message(role: :system, content: "Bookmarks exported to: #{path}")
129
130
  :handled
130
131
  rescue StandardError => e
132
+ Legion::Logging.warn("handle_bookmark failed: #{e.message}") if defined?(Legion::Logging)
131
133
  @message_stream.add_message(role: :system, content: "Bookmark export failed: #{e.message}")
132
134
  :handled
133
135
  end
@@ -150,6 +152,7 @@ module Legion
150
152
  end
151
153
  :handled
152
154
  rescue StandardError => e
155
+ Legion::Logging.warn("handle_tee failed: #{e.message}") if defined?(Legion::Logging)
153
156
  @message_stream.add_message(role: :system, content: "Tee error: #{e.message}")
154
157
  :handled
155
158
  end
@@ -158,7 +161,8 @@ module Legion
158
161
  return unless @tee_path
159
162
 
160
163
  File.open(@tee_path, 'a') { |f| f.puts(line) }
161
- rescue StandardError
164
+ rescue StandardError => e
165
+ Legion::Logging.warn("tee_message failed: #{e.message}") if defined?(Legion::Logging)
162
166
  nil
163
167
  end
164
168
  end
@@ -98,6 +98,7 @@ module Legion
98
98
  display_grep_results(results, pattern_str)
99
99
  :handled
100
100
  rescue RegexpError => e
101
+ Legion::Logging.warn("handle_grep regex error: #{e.message}") if defined?(Legion::Logging)
101
102
  @message_stream.add_message(role: :system, content: "Invalid regex: #{e.message}")
102
103
  :handled
103
104
  end
@@ -300,10 +301,12 @@ module Legion
300
301
 
301
302
  def copy_to_clipboard(text)
302
303
  IO.popen('pbcopy', 'w') { |io| io.write(text) }
303
- rescue Errno::ENOENT
304
+ rescue Errno::ENOENT => e
305
+ Legion::Logging.debug("pbcopy not available: #{e.message}") if defined?(Legion::Logging)
304
306
  begin
305
307
  IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) }
306
- rescue Errno::ENOENT
308
+ rescue Errno::ENOENT => e
309
+ Legion::Logging.debug("xclip not available: #{e.message}") if defined?(Legion::Logging)
307
310
  nil
308
311
  end
309
312
  end
@@ -394,7 +397,8 @@ module Legion
394
397
  raw = File.read(favorites_file)
395
398
  parsed = ::JSON.parse(raw, symbolize_names: true)
396
399
  parsed.is_a?(Array) ? parsed : []
397
- rescue StandardError
400
+ rescue StandardError => e
401
+ Legion::Logging.warn("load_favorites failed: #{e.message}") if defined?(Legion::Logging)
398
402
  []
399
403
  end
400
404
 
@@ -25,6 +25,7 @@ module Legion
25
25
 
26
26
  apply_model_switch(name)
27
27
  rescue StandardError => e
28
+ Legion::Logging.warn("switch_model failed: #{e.message}") if defined?(Legion::Logging)
28
29
  @message_stream.add_message(role: :system, content: "Failed to switch model: #{e.message}")
29
30
  end
30
31
 
@@ -53,7 +54,8 @@ module Legion
53
54
  return nil unless providers.is_a?(Hash) && providers.key?(name.to_sym)
54
55
 
55
56
  Legion::LLM.chat(provider: name)
56
- rescue StandardError
57
+ rescue StandardError => e
58
+ Legion::Logging.warn("try_provider_switch failed: #{e.message}") if defined?(Legion::Logging)
57
59
  nil
58
60
  end
59
61
 
@@ -141,7 +141,8 @@ module Legion
141
141
  auto_save_session
142
142
  @last_autosave = Time.now
143
143
  @status_bar.notify(message: 'Autosaved', level: :info, ttl: 2)
144
- rescue StandardError
144
+ rescue StandardError => e
145
+ Legion::Logging.warn("check_autosave failed: #{e.message}") if defined?(Legion::Logging)
145
146
  nil
146
147
  end
147
148
 
@@ -152,7 +153,8 @@ module Legion
152
153
  @session_name = @session_store.auto_session_name(messages: @message_stream.messages)
153
154
  end
154
155
  @session_store.save(@session_name, messages: @message_stream.messages)
155
- rescue StandardError
156
+ rescue StandardError => e
157
+ Legion::Logging.warn("auto_save_session failed: #{e.message}") if defined?(Legion::Logging)
156
158
  nil
157
159
  end
158
160
 
@@ -102,7 +102,8 @@ module Legion
102
102
  screen = Screens::Extensions.new(@app, output: @output)
103
103
  @app.screen_manager.push(screen)
104
104
  :handled
105
- rescue LoadError
105
+ rescue LoadError => e
106
+ Legion::Logging.debug("extensions screen not available: #{e.message}") if defined?(Legion::Logging)
106
107
  @message_stream.add_message(role: :system, content: 'Extensions screen not available.')
107
108
  :handled
108
109
  end
@@ -112,7 +113,8 @@ module Legion
112
113
  screen = Screens::Config.new(@app, output: @output)
113
114
  @app.screen_manager.push(screen)
114
115
  :handled
115
- rescue LoadError
116
+ rescue LoadError => e
117
+ Legion::Logging.debug("config screen not available: #{e.message}") if defined?(Legion::Logging)
116
118
  @message_stream.add_message(role: :system, content: 'Config screen not available.')
117
119
  :handled
118
120
  end
@@ -479,6 +481,7 @@ module Legion
479
481
  @message_stream.add_message(role: :system, content: "= #{result}")
480
482
  :handled
481
483
  rescue SyntaxError, ZeroDivisionError, Math::DomainError => e
484
+ Legion::Logging.warn("handle_calc error: #{e.message}") if defined?(Legion::Logging)
482
485
  @message_stream.add_message(role: :system, content: "Error: #{e.message}")
483
486
  :handled
484
487
  end
@@ -641,6 +644,7 @@ module Legion
641
644
  end
642
645
  result.to_s.chomp
643
646
  rescue StandardError => e
647
+ Legion::Logging.warn("pipe_through_command failed: #{e.message}") if defined?(Legion::Logging)
644
648
  raise "command failed: #{e.message}"
645
649
  end
646
650
 
@@ -652,6 +656,7 @@ module Legion
652
656
  @message_stream.add_message(role: :system, content: "#{path}:\n#{entries.join("\n")}")
653
657
  :handled
654
658
  rescue Errno::ENOENT, Errno::EACCES => e
659
+ Legion::Logging.warn("handle_ls failed: #{e.message}") if defined?(Legion::Logging)
655
660
  @message_stream.add_message(role: :system, content: "ls: #{e.message}")
656
661
  :handled
657
662
  end
@@ -837,7 +842,8 @@ module Legion
837
842
 
838
843
  require 'json'
839
844
  ::JSON.parse(File.read(prefs_path))
840
- rescue ::JSON::ParserError
845
+ rescue ::JSON::ParserError => e
846
+ Legion::Logging.warn("load_prefs failed: #{e.message}") if defined?(Legion::Logging)
841
847
  {}
842
848
  end
843
849
 
@@ -5,6 +5,7 @@ require_relative '../components/message_stream'
5
5
  require_relative '../components/status_bar'
6
6
  require_relative '../components/input_bar'
7
7
  require_relative '../components/token_tracker'
8
+ require_relative '../components/tool_call_parser'
8
9
  require_relative '../theme'
9
10
  require_relative 'chat/session_commands'
10
11
  require_relative 'chat/export_commands'
@@ -193,6 +194,7 @@ module Legion
193
194
  send_via_direct(message)
194
195
  end
195
196
  rescue StandardError => e
197
+ Legion::Logging.error("send_to_llm failed: #{e.message}") if defined?(Legion::Logging)
196
198
  @status_bar.update(thinking: false)
197
199
  @message_stream.append_streaming("\n[Error: #{e.message}]")
198
200
  end
@@ -258,14 +260,17 @@ module Legion
258
260
 
259
261
  case result&.dig(:status)
260
262
  when :done
261
- @message_stream.append_streaming(result[:response])
263
+ parser = build_tool_call_parser
264
+ parser.feed(result[:response])
265
+ parser.flush
262
266
  when :error
263
267
  err = result.dig(:error, :message) || 'Unknown error'
264
268
  @message_stream.append_streaming("\n[Daemon error: #{err}]")
265
269
  else
266
270
  send_via_direct(message)
267
271
  end
268
- rescue StandardError
272
+ rescue StandardError => e
273
+ Legion::Logging.warn("send_via_daemon failed: #{e.message}") if defined?(Legion::Logging)
269
274
  send_via_direct(message)
270
275
  end
271
276
 
@@ -277,14 +282,16 @@ module Legion
277
282
  render_screen
278
283
  start_time = Time.now
279
284
  response_text = +''
285
+ parser = build_tool_call_parser
280
286
  response = @llm_chat.ask(message) do |chunk|
281
287
  @status_bar.update(thinking: false)
282
288
  if chunk.content
283
289
  response_text << chunk.content
284
- @message_stream.append_streaming(chunk.content)
290
+ parser.feed(chunk.content)
285
291
  end
286
292
  render_screen
287
293
  end
294
+ parser.flush
288
295
  record_response_time(Time.now - start_time)
289
296
  @status_bar.update(thinking: false)
290
297
  track_response_tokens(response)
@@ -296,7 +303,8 @@ module Legion
296
303
  return unless RUBY_PLATFORM =~ /darwin/
297
304
 
298
305
  ::Process.spawn('say', text[0..500], err: '/dev/null', out: '/dev/null')
299
- rescue StandardError
306
+ rescue StandardError => e
307
+ Legion::Logging.debug("speak_response failed: #{e.message}") if defined?(Legion::Logging)
300
308
  nil
301
309
  end
302
310
 
@@ -378,7 +386,8 @@ module Legion
378
386
  @output.print ::TTY::Cursor.move_to(2, start_row + i)
379
387
  @output.print line
380
388
  end
381
- rescue StandardError
389
+ rescue StandardError => e
390
+ Legion::Logging.warn("render_overlay failed: #{e.message}") if defined?(Legion::Logging)
382
391
  nil
383
392
  end
384
393
  # rubocop:enable Metrics/AbcSize
@@ -388,7 +397,8 @@ module Legion
388
397
  return read_multiline_input if @multiline_mode
389
398
 
390
399
  @input_bar.read_line
391
- rescue Interrupt
400
+ rescue Interrupt => e
401
+ Legion::Logging.debug("read_input interrupted: #{e.message}") if defined?(Legion::Logging)
392
402
  nil
393
403
  end
394
404
 
@@ -402,7 +412,8 @@ module Legion
402
412
  lines << line
403
413
  end
404
414
  lines.empty? ? nil : lines.join("\n")
405
- rescue Interrupt
415
+ rescue Interrupt => e
416
+ Legion::Logging.debug("read_multiline_input interrupted: #{e.message}") if defined?(Legion::Logging)
406
417
  nil
407
418
  end
408
419
 
@@ -617,6 +628,19 @@ module Legion
617
628
  "autosave:#{@autosave_enabled}"
618
629
  end
619
630
 
631
+ def build_tool_call_parser
632
+ Components::ToolCallParser.new(
633
+ on_text: lambda { |text|
634
+ last = @message_stream.messages.last
635
+ @message_stream.add_message(role: :assistant, content: '') if last && last[:tool_panel]
636
+ @message_stream.append_streaming(text)
637
+ },
638
+ on_tool_call: lambda { |name:, args:|
639
+ @message_stream.add_tool_call(name: name, args: args, status: :complete)
640
+ }
641
+ )
642
+ end
643
+
620
644
  def build_default_input_bar
621
645
  cfg = safe_config
622
646
  name = cfg[:name] || 'User'
@@ -626,14 +650,16 @@ module Legion
626
650
  def terminal_width
627
651
  require 'tty-screen'
628
652
  ::TTY::Screen.width
629
- rescue StandardError
653
+ rescue StandardError => e
654
+ Legion::Logging.debug("terminal_width failed: #{e.message}") if defined?(Legion::Logging)
630
655
  80
631
656
  end
632
657
 
633
658
  def terminal_height
634
659
  require 'tty-screen'
635
660
  ::TTY::Screen.height
636
- rescue StandardError
661
+ rescue StandardError => e
662
+ Legion::Logging.debug("terminal_height failed: #{e.message}") if defined?(Legion::Logging)
637
663
  24
638
664
  end
639
665
 
@@ -100,7 +100,8 @@ module Legion
100
100
  @file_data = ::JSON.parse(File.read(path))
101
101
  @viewing_file = true
102
102
  @selected_key = 0
103
- rescue ::JSON::ParserError, Errno::ENOENT
103
+ rescue ::JSON::ParserError, Errno::ENOENT => e
104
+ Legion::Logging.warn("open_file failed: #{e.message}") if defined?(Legion::Logging)
104
105
  @file_data = { 'error' => 'Failed to parse file' }
105
106
  @viewing_file = true
106
107
  end
@@ -123,7 +124,8 @@ module Legion
123
124
  return unless validate_config(@file_data)
124
125
 
125
126
  save_current_file
126
- rescue ::TTY::Reader::InputInterrupt, Interrupt
127
+ rescue ::TTY::Reader::InputInterrupt, Interrupt => e
128
+ Legion::Logging.debug("edit_selected_key cancelled: #{e.message}") if defined?(Legion::Logging)
127
129
  nil
128
130
  end
129
131
 
@@ -131,6 +133,7 @@ module Legion
131
133
  ::JSON.generate(data)
132
134
  true
133
135
  rescue StandardError => e
136
+ Legion::Logging.warn("validate_config failed: #{e.message}") if defined?(Legion::Logging)
134
137
  @messages = ["Invalid JSON: #{e.message}"]
135
138
  false
136
139
  end
@@ -215,11 +215,12 @@ module Legion
215
215
  else
216
216
  :pass
217
217
  end
218
- rescue LoadError, StandardError
218
+ rescue LoadError, StandardError => e
219
+ Legion::Logging.debug("extensions_shortcut failed: #{e.message}") if defined?(Legion::Logging)
219
220
  :pass
220
221
  end
221
222
 
222
- # rubocop:disable Metrics/AbcSize
223
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
223
224
  def llm_info
224
225
  info = { provider: 'none', model: nil, started: false, daemon: false }
225
226
  if defined?(Legion::LLM)
@@ -233,11 +234,12 @@ module Legion
233
234
  Legion::LLM::DaemonClient.available?
234
235
  end
235
236
  info
236
- rescue StandardError
237
+ rescue StandardError => e
238
+ Legion::Logging.warn("llm_info failed: #{e.message}") if defined?(Legion::Logging)
237
239
  info
238
240
  end
239
241
 
240
- # rubocop:enable Metrics/AbcSize
242
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
241
243
 
242
244
  def probe_services
243
245
  require 'socket'
@@ -252,13 +254,15 @@ module Legion
252
254
 
253
255
  def port_open?(port)
254
256
  ::Socket.tcp('127.0.0.1', port, connect_timeout: 0.5) { true }
255
- rescue StandardError
257
+ rescue StandardError => e
258
+ Legion::Logging.debug("port_open? #{port} failed: #{e.message}") if defined?(Legion::Logging)
256
259
  false
257
260
  end
258
261
 
259
262
  def discover_extensions
260
263
  Gem::Specification.select { |s| s.name.start_with?('lex-') }.map(&:name).sort
261
- rescue StandardError
264
+ rescue StandardError => e
265
+ Legion::Logging.warn("discover_extensions failed: #{e.message}") if defined?(Legion::Logging)
262
266
  []
263
267
  end
264
268
 
@@ -270,7 +274,8 @@ module Legion
270
274
  pid: ::Process.pid,
271
275
  memory: format_memory
272
276
  }
273
- rescue StandardError
277
+ rescue StandardError => e
278
+ Legion::Logging.warn("system_info failed: #{e.message}") if defined?(Legion::Logging)
274
279
  {}
275
280
  end
276
281
 
@@ -283,7 +288,8 @@ module Legion
283
288
  rss_kb = match ? match[1].to_i : 0
284
289
  "#{(rss_kb / 1024.0).round(1)} MB"
285
290
  end
286
- rescue StandardError
291
+ rescue StandardError => e
292
+ Legion::Logging.debug("format_memory failed: #{e.message}") if defined?(Legion::Logging)
287
293
  'unknown'
288
294
  end
289
295
 
@@ -292,7 +298,8 @@ module Legion
292
298
  return [] unless File.exist?(log_path)
293
299
 
294
300
  File.readlines(log_path, chomp: true).last(20)
295
- rescue StandardError
301
+ rescue StandardError => e
302
+ Legion::Logging.warn("recent_activity failed: #{e.message}") if defined?(Legion::Logging)
296
303
  []
297
304
  end
298
305
  end
@@ -171,7 +171,8 @@ module Legion
171
171
  return unless entry && entry[:homepage]
172
172
 
173
173
  system_open(entry[:homepage])
174
- rescue StandardError
174
+ rescue StandardError => e
175
+ Legion::Logging.warn("open_homepage failed: #{e.message}") if defined?(Legion::Logging)
175
176
  nil
176
177
  end
177
178
 
@@ -44,6 +44,7 @@ module Legion
44
44
  run_cache_awakening(scan_data)
45
45
  run_gaia_awakening
46
46
  run_extension_detection
47
+ run_service_auth
47
48
  run_reveal(name: config[:name], scan_data: scan_data, github_data: github_data)
48
49
  @log.log('onboarding', 'activate complete')
49
50
  build_onboarding_result(config, scan_data, github_data)
@@ -327,6 +328,78 @@ module Legion
327
328
  end
328
329
  # rubocop:enable Metrics/AbcSize
329
330
 
331
+ def run_service_auth
332
+ run_teams_auth if teams_detected? && teams_gem_loadable? && !teams_already_authenticated?
333
+ end
334
+
335
+ def run_teams_auth
336
+ return unless @wizard.confirm('I see Microsoft Teams on your system. Connect to it?')
337
+
338
+ typed_output('Connecting to Microsoft Teams...')
339
+ @output.puts
340
+ browser_auth = build_teams_browser_auth
341
+ result = browser_auth.authenticate
342
+ if result && result[:access_token]
343
+ store_teams_token(result)
344
+ typed_output('Teams connected.')
345
+ else
346
+ typed_output('Teams connection skipped.')
347
+ end
348
+ @output.puts
349
+ rescue StandardError => e
350
+ typed_output("Teams connection failed: #{e.message}")
351
+ @output.puts
352
+ end
353
+
354
+ def teams_detected?
355
+ return false unless defined?(@detect_queue)
356
+
357
+ detect_result = drain_with_timeout(@detect_queue, timeout: 0)
358
+ return false unless detect_result
359
+
360
+ results = detect_result[:data] || []
361
+ results.any? { |d| d[:name] == 'Microsoft Teams' }
362
+ rescue StandardError => e
363
+ Legion::Logging.debug("teams_detected? failed: #{e.message}") if defined?(Legion::Logging)
364
+ false
365
+ end
366
+
367
+ def teams_gem_loadable?
368
+ Gem::Specification.find_by_name('lex-microsoft_teams')
369
+ true
370
+ rescue Gem::MissingSpecError => e
371
+ Legion::Logging.debug("lex-microsoft_teams not installed: #{e.message}") if defined?(Legion::Logging)
372
+ false
373
+ end
374
+
375
+ def teams_already_authenticated?
376
+ File.exist?(File.expand_path('~/.legionio/tokens/microsoft_teams.json'))
377
+ end
378
+
379
+ def build_teams_browser_auth
380
+ require 'legion/extensions/microsoft_teams/helpers/browser_auth'
381
+ settings = begin
382
+ Legion::Settings.dig(:microsoft_teams, :auth) || {}
383
+ rescue StandardError => e
384
+ Legion::Logging.warn("build_teams_browser_auth settings failed: #{e.message}") if defined?(Legion::Logging)
385
+ {}
386
+ end
387
+ Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
388
+ tenant_id: settings[:tenant_id],
389
+ client_id: settings[:client_id]
390
+ )
391
+ end
392
+
393
+ def store_teams_token(result)
394
+ require 'legion/extensions/microsoft_teams/helpers/token_cache'
395
+ cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
396
+ cache.store_delegated_token(result)
397
+ cache.save_to_vault
398
+ rescue StandardError => e
399
+ Legion::Logging.warn("store_teams_token failed: #{e.message}") if defined?(Legion::Logging)
400
+ nil
401
+ end
402
+
330
403
  def detect_gem_available?
331
404
  require 'legion/extensions/detect'
332
405
  true
@@ -376,13 +449,15 @@ module Legion
376
449
 
377
450
  clusters = Legion::Settings.dig(:crypt, :vault, :clusters)
378
451
  clusters.is_a?(Hash) && clusters.any?
379
- rescue StandardError
452
+ rescue StandardError => e
453
+ Legion::Logging.warn("vault_clusters_configured? failed: #{e.message}") if defined?(Legion::Logging)
380
454
  false
381
455
  end
382
456
 
383
457
  def vault_cluster_count
384
458
  Legion::Settings.dig(:crypt, :vault, :clusters)&.size || 0
385
- rescue StandardError
459
+ rescue StandardError => e
460
+ Legion::Logging.warn("vault_cluster_count failed: #{e.message}") if defined?(Legion::Logging)
386
461
  0
387
462
  end
388
463
 
@@ -779,7 +854,8 @@ module Legion
779
854
 
780
855
  sleep 0.1
781
856
  end
782
- rescue ThreadError
857
+ rescue ThreadError => e
858
+ Legion::Logging.debug("drain_with_timeout interrupted: #{e.message}") if defined?(Legion::Logging)
783
859
  nil
784
860
  end
785
861
 
@@ -801,6 +877,7 @@ module Legion
801
877
  false
802
878
  end
803
879
 
880
+ # rubocop:disable Metrics/AbcSize
804
881
  def legionio_running?
805
882
  pid_paths = [
806
883
  File.expand_path('~/.legionio/legion.pid'),
@@ -815,16 +892,20 @@ module Legion
815
892
  begin
816
893
  ::Process.kill(0, pid)
817
894
  return true
818
- rescue Errno::ESRCH
895
+ rescue Errno::ESRCH => e
896
+ Legion::Logging.debug("pid #{pid} not running: #{e.message}") if defined?(Legion::Logging)
819
897
  next
820
- rescue Errno::EPERM
898
+ rescue Errno::EPERM => e
899
+ Legion::Logging.debug("pid #{pid} exists (no permission): #{e.message}") if defined?(Legion::Logging)
821
900
  return true
822
901
  end
823
902
  end
824
903
  system('pgrep -x legionio > /dev/null 2>&1')
825
- rescue StandardError
904
+ rescue StandardError => e
905
+ Legion::Logging.warn("legionio_running? failed: #{e.message}") if defined?(Legion::Logging)
826
906
  false
827
907
  end
908
+ # rubocop:enable Metrics/AbcSize
828
909
 
829
910
  def start_legionio_daemon
830
911
  @log.log('gaia', 'attempting to start legionio daemon')
@@ -846,14 +927,16 @@ module Legion
846
927
  def terminal_width
847
928
  require 'tty-screen'
848
929
  ::TTY::Screen.width
849
- rescue StandardError
930
+ rescue StandardError => e
931
+ Legion::Logging.debug("terminal_width failed: #{e.message}") if defined?(Legion::Logging)
850
932
  80
851
933
  end
852
934
 
853
935
  def terminal_height
854
936
  require 'tty-screen'
855
937
  ::TTY::Screen.height
856
- rescue StandardError
938
+ rescue StandardError => e
939
+ Legion::Logging.debug("terminal_height failed: #{e.message}") if defined?(Legion::Logging)
857
940
  24
858
941
  end
859
942
  end
@@ -31,7 +31,8 @@ module Legion
31
31
  data = ::JSON.parse(File.read(path), symbolize_names: true)
32
32
  data[:messages] = data[:messages].map { |m| deserialize_message(m) }
33
33
  data
34
- rescue ::JSON::ParserError
34
+ rescue ::JSON::ParserError => e
35
+ Legion::Logging.warn("session load failed: #{e.message}") if defined?(Legion::Logging)
35
36
  nil
36
37
  end
37
38
 
@@ -40,7 +41,8 @@ module Legion
40
41
  name = File.basename(path, '.json')
41
42
  data = ::JSON.parse(File.read(path), symbolize_names: true)
42
43
  { name: name, saved_at: data[:saved_at], message_count: data[:messages]&.size || 0 }
43
- rescue StandardError
44
+ rescue StandardError => e
45
+ Legion::Logging.warn("session list entry failed: #{e.message}") if defined?(Legion::Logging)
44
46
  { name: name, saved_at: nil, message_count: 0 }
45
47
  end
46
48
  entries.sort_by { |s| s[:saved_at] || '' }.reverse
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.28'
5
+ VERSION = '0.4.30'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-tty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.28
4
+ version: 0.4.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -177,6 +177,34 @@ dependencies:
177
177
  - - ">="
178
178
  - !ruby/object:Gem::Version
179
179
  version: '1.18'
180
+ - !ruby/object:Gem::Dependency
181
+ name: legion-json
182
+ requirement: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '1.2'
187
+ type: :runtime
188
+ prerelease: false
189
+ version_requirements: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '1.2'
194
+ - !ruby/object:Gem::Dependency
195
+ name: legion-logging
196
+ requirement: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0.3'
201
+ type: :runtime
202
+ prerelease: false
203
+ version_requirements: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0.3'
180
208
  description: Rich TUI with onboarding wizard, AI chat shell, and operational dashboards
181
209
  for LegionIO
182
210
  email:
@@ -213,6 +241,7 @@ files:
213
241
  - lib/legion/tty/components/status_bar.rb
214
242
  - lib/legion/tty/components/table_view.rb
215
243
  - lib/legion/tty/components/token_tracker.rb
244
+ - lib/legion/tty/components/tool_call_parser.rb
216
245
  - lib/legion/tty/components/tool_panel.rb
217
246
  - lib/legion/tty/components/wizard_prompt.rb
218
247
  - lib/legion/tty/daemon_client.rb