legion-tty 0.4.28 → 0.4.29

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: 6f5fae7ebc821ab002ba505d7570c4b3116cd8cb9ebe2fe663aa63e93c5e459e
4
+ data.tar.gz: 127da7a51afc0eb408b63dbc32c6c0df7971fc767938a5e2d6ac6c435e6742ae
5
5
  SHA512:
6
- metadata.gz: 6bf396cf31f2c03c3db4b7569ed1d6c79b26506bb2316a0e05111538b35c8005d40104cdd9cb31937ed5046fe29c5f4e14b4cac33eddb897f7681e70187df132
7
- data.tar.gz: 3f1d3a31eb2e636d4b6df7e990ba03f8f690ef31ab4bb2d1ee153e597d94698bded853e31b779a62e56eb88bae9a5423d8832f66577be0a7a053cb4a36507829
6
+ metadata.gz: 97c6604bd14b08983fb7f189be0150e111b69d59b5d55ce0768f72b5fa8612f596979979aed0e2c93a93461c56cfbcb9b37bf1938b8f6a9c41054470331acbcb
7
+ data.tar.gz: 36630d28f144a4c15fa08ac9a93f476161be4dc6dc42bc0045cabef31a5e1cfeb11ec01e80875cfb4d628735951bc42d90203959505faed4cf44b03429291bae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.29] - 2026-03-20
4
+
5
+ ### Fixed
6
+ - RuboCop `Style/SingleArgumentDig` offense in onboarding teams detection
7
+ - RuboCop `Layout/LineLength` offense in onboarding teams spec
8
+
3
9
  ## [0.4.28] - 2026-03-19
4
10
 
5
11
  ### 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
 
@@ -0,0 +1,113 @@
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
100
+ @on_text.call("#{OPEN_TAG}#{json_str}#{CLOSE_TAG}")
101
+ end
102
+
103
+ def partial_open_match?
104
+ tag = OPEN_TAG
105
+ (1...([tag.length, @working.length].min + 1)).any? do |len|
106
+ suffix = @working[-len..]
107
+ tag.start_with?(suffix) && suffix.length < tag.length
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ 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
@@ -35,7 +35,7 @@ module Legion
35
35
  end
36
36
  return nil unless response.code.to_i == 200
37
37
 
38
- body = ::JSON.parse(response.body, symbolize_names: true)
38
+ body = Legion::JSON.load(response.body)
39
39
  @manifest = body[:data]
40
40
  write_cache(@manifest)
41
41
  @manifest
@@ -48,7 +48,7 @@ module Legion
48
48
 
49
49
  return nil unless @cache_file && File.exist?(@cache_file)
50
50
 
51
- @manifest = ::JSON.parse(File.read(@cache_file), symbolize_names: true)
51
+ @manifest = Legion::JSON.load(File.read(@cache_file))
52
52
  rescue StandardError
53
53
  nil
54
54
  end
@@ -75,12 +75,12 @@ module Legion
75
75
  return nil unless available?
76
76
 
77
77
  uri = URI("#{daemon_url}/api/llm/chat")
78
- payload = ::JSON.dump({ message: message, model: model, provider: provider })
78
+ payload = Legion::JSON.dump({ message: message, model: model, provider: provider })
79
79
  response = post_json(uri, payload)
80
80
 
81
81
  return nil unless response && SUCCESS_CODES.include?(response.code.to_i)
82
82
 
83
- ::JSON.parse(response.body, symbolize_names: true)
83
+ Legion::JSON.load(response.body)
84
84
  rescue StandardError
85
85
  nil
86
86
  end
@@ -109,7 +109,7 @@ module Legion
109
109
  return unless @cache_file
110
110
 
111
111
  FileUtils.mkdir_p(File.dirname(@cache_file))
112
- File.write(@cache_file, ::JSON.dump(data))
112
+ File.write(@cache_file, Legion::JSON.dump(data))
113
113
  rescue StandardError
114
114
  nil
115
115
  end
@@ -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'
@@ -258,7 +259,9 @@ module Legion
258
259
 
259
260
  case result&.dig(:status)
260
261
  when :done
261
- @message_stream.append_streaming(result[:response])
262
+ parser = build_tool_call_parser
263
+ parser.feed(result[:response])
264
+ parser.flush
262
265
  when :error
263
266
  err = result.dig(:error, :message) || 'Unknown error'
264
267
  @message_stream.append_streaming("\n[Daemon error: #{err}]")
@@ -277,14 +280,16 @@ module Legion
277
280
  render_screen
278
281
  start_time = Time.now
279
282
  response_text = +''
283
+ parser = build_tool_call_parser
280
284
  response = @llm_chat.ask(message) do |chunk|
281
285
  @status_bar.update(thinking: false)
282
286
  if chunk.content
283
287
  response_text << chunk.content
284
- @message_stream.append_streaming(chunk.content)
288
+ parser.feed(chunk.content)
285
289
  end
286
290
  render_screen
287
291
  end
292
+ parser.flush
288
293
  record_response_time(Time.now - start_time)
289
294
  @status_bar.update(thinking: false)
290
295
  track_response_tokens(response)
@@ -617,6 +622,19 @@ module Legion
617
622
  "autosave:#{@autosave_enabled}"
618
623
  end
619
624
 
625
+ def build_tool_call_parser
626
+ Components::ToolCallParser.new(
627
+ on_text: lambda { |text|
628
+ last = @message_stream.messages.last
629
+ @message_stream.add_message(role: :assistant, content: '') if last && last[:tool_panel]
630
+ @message_stream.append_streaming(text)
631
+ },
632
+ on_tool_call: lambda { |name:, args:|
633
+ @message_stream.add_tool_call(name: name, args: args, status: :complete)
634
+ }
635
+ )
636
+ end
637
+
620
638
  def build_default_input_bar
621
639
  cfg = safe_config
622
640
  name = cfg[:name] || 'User'
@@ -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,74 @@ 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
363
+ false
364
+ end
365
+
366
+ def teams_gem_loadable?
367
+ Gem::Specification.find_by_name('lex-microsoft_teams')
368
+ true
369
+ rescue Gem::MissingSpecError
370
+ false
371
+ end
372
+
373
+ def teams_already_authenticated?
374
+ File.exist?(File.expand_path('~/.legionio/tokens/microsoft_teams.json'))
375
+ end
376
+
377
+ def build_teams_browser_auth
378
+ require 'legion/extensions/microsoft_teams/helpers/browser_auth'
379
+ settings = begin
380
+ Legion::Settings.dig(:microsoft_teams, :auth) || {}
381
+ rescue StandardError
382
+ {}
383
+ end
384
+ Legion::Extensions::MicrosoftTeams::Helpers::BrowserAuth.new(
385
+ tenant_id: settings[:tenant_id],
386
+ client_id: settings[:client_id]
387
+ )
388
+ end
389
+
390
+ def store_teams_token(result)
391
+ require 'legion/extensions/microsoft_teams/helpers/token_cache'
392
+ cache = Legion::Extensions::MicrosoftTeams::Helpers::TokenCache.new
393
+ cache.store_delegated_token(result)
394
+ cache.save_to_vault
395
+ rescue StandardError
396
+ nil
397
+ end
398
+
330
399
  def detect_gem_available?
331
400
  require 'legion/extensions/detect'
332
401
  true
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.28'
5
+ VERSION = '0.4.29'
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.29
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