legion-tty 0.4.27 → 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: f1f14c29bb844005733b887586662c8c08947f757567ec9438029b423b0c991e
4
- data.tar.gz: 3e0244816b27e6174541fceba9aea3df6d87a7698f40766cf7341cf12b8ab0b0
3
+ metadata.gz: 6f5fae7ebc821ab002ba505d7570c4b3116cd8cb9ebe2fe663aa63e93c5e459e
4
+ data.tar.gz: 127da7a51afc0eb408b63dbc32c6c0df7971fc767938a5e2d6ac6c435e6742ae
5
5
  SHA512:
6
- metadata.gz: 3af49d57dacb01af6ffa504d2e547d0ca920e0b675c119cdfdf95bf6f5926e5ba3f212144e3f139c005929abd52c147fa57dc06848aaae0a098a2444b84db20a
7
- data.tar.gz: cb4b0902b3c3669231ff0ffc9bbe7b198f334fa8f3f68454a79345f808313c4f13ad63ea3c01efb36225beac388b84b28dc8c2a3d2558aead4f8c0a61acb69e6
6
+ metadata.gz: 97c6604bd14b08983fb7f189be0150e111b69d59b5d55ce0768f72b5fa8612f596979979aed0e2c93a93461c56cfbcb9b37bf1938b8f6a9c41054470331acbcb
7
+ data.tar.gz: 36630d28f144a4c15fa08ac9a93f476161be4dc6dc42bc0045cabef31a5e1cfeb11ec01e80875cfb4d628735951bc42d90203959505faed4cf44b03429291bae
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
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
+
9
+ ## [0.4.28] - 2026-03-19
10
+
11
+ ### Added
12
+ - `Legion::TTY::DaemonClient` module for daemon-first manifest fetching, intent matching, and LLM routing
13
+ - Manifest caching to `~/.legionio/catalog.json` with intent confidence threshold matching
14
+ - `chat` method for daemon-routed LLM requests via `/api/llm/chat`
15
+
3
16
  ## [0.4.27] - 2026-03-19
4
17
 
5
18
  ### 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
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'fileutils'
6
+ require 'legion/json'
7
+
8
+ module Legion
9
+ module TTY
10
+ module DaemonClient
11
+ SUCCESS_CODES = [200, 201, 202].freeze
12
+
13
+ class << self
14
+ def configure(daemon_url: 'http://127.0.0.1:4567', cache_file: nil, timeout: 5)
15
+ @daemon_url = daemon_url
16
+ @cache_file = cache_file || File.expand_path('~/.legionio/catalog.json')
17
+ @timeout = timeout
18
+ @manifest = nil
19
+ end
20
+
21
+ def available?
22
+ uri = URI("#{daemon_url}/api/health")
23
+ response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: @timeout, read_timeout: @timeout) do |http|
24
+ http.get(uri.path)
25
+ end
26
+ response.code.to_i == 200
27
+ rescue StandardError
28
+ false
29
+ end
30
+
31
+ def fetch_manifest
32
+ uri = URI("#{daemon_url}/api/catalog")
33
+ response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: @timeout, read_timeout: @timeout) do |http|
34
+ http.get(uri.path)
35
+ end
36
+ return nil unless response.code.to_i == 200
37
+
38
+ body = Legion::JSON.load(response.body)
39
+ @manifest = body[:data]
40
+ write_cache(@manifest)
41
+ @manifest
42
+ rescue StandardError
43
+ nil
44
+ end
45
+
46
+ def cached_manifest
47
+ return @manifest if @manifest
48
+
49
+ return nil unless @cache_file && File.exist?(@cache_file)
50
+
51
+ @manifest = Legion::JSON.load(File.read(@cache_file))
52
+ rescue StandardError
53
+ nil
54
+ end
55
+
56
+ def manifest
57
+ @manifest || cached_manifest
58
+ end
59
+
60
+ def match_intent(intent_text)
61
+ return nil unless manifest
62
+
63
+ normalized = intent_text.downcase.strip
64
+ manifest.each do |ext|
65
+ next unless ext[:known_intents]
66
+
67
+ ext[:known_intents].each do |ki|
68
+ return ki if ki[:intent]&.downcase&.strip == normalized && ki[:confidence] >= 0.8
69
+ end
70
+ end
71
+ nil
72
+ end
73
+
74
+ def chat(message:, model: nil, provider: nil)
75
+ return nil unless available?
76
+
77
+ uri = URI("#{daemon_url}/api/llm/chat")
78
+ payload = Legion::JSON.dump({ message: message, model: model, provider: provider })
79
+ response = post_json(uri, payload)
80
+
81
+ return nil unless response && SUCCESS_CODES.include?(response.code.to_i)
82
+
83
+ Legion::JSON.load(response.body)
84
+ rescue StandardError
85
+ nil
86
+ end
87
+
88
+ def reset!
89
+ @daemon_url = nil
90
+ @cache_file = nil
91
+ @timeout = nil
92
+ @manifest = nil
93
+ end
94
+
95
+ private
96
+
97
+ def daemon_url
98
+ @daemon_url || 'http://127.0.0.1:4567'
99
+ end
100
+
101
+ def post_json(uri, body)
102
+ req = Net::HTTP::Post.new(uri)
103
+ req['Content-Type'] = 'application/json'
104
+ req.body = body
105
+ Net::HTTP.start(uri.hostname, uri.port, open_timeout: @timeout, read_timeout: @timeout) { |h| h.request(req) }
106
+ end
107
+
108
+ def write_cache(data)
109
+ return unless @cache_file
110
+
111
+ FileUtils.mkdir_p(File.dirname(@cache_file))
112
+ File.write(@cache_file, Legion::JSON.dump(data))
113
+ rescue StandardError
114
+ nil
115
+ end
116
+ end
117
+ end
118
+ end
119
+ 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.27'
5
+ VERSION = '0.4.29'
6
6
  end
7
7
  end
data/lib/legion/tty.rb CHANGED
@@ -15,6 +15,7 @@ require_relative 'tty/components/token_tracker'
15
15
  require_relative 'tty/components/tool_panel'
16
16
  require_relative 'tty/components/wizard_prompt'
17
17
  require_relative 'tty/session_store'
18
+ require_relative 'tty/daemon_client'
18
19
  require_relative 'tty/background/scanner'
19
20
  require_relative 'tty/background/github_probe'
20
21
  require_relative 'tty/background/kerberos_probe'
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.27
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,8 +241,10 @@ 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
247
+ - lib/legion/tty/daemon_client.rb
218
248
  - lib/legion/tty/hotkeys.rb
219
249
  - lib/legion/tty/screen_manager.rb
220
250
  - lib/legion/tty/screens/base.rb