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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +1 -1
- data/lib/legion/tty/components/tool_call_parser.rb +113 -0
- data/lib/legion/tty/daemon_client.rb +6 -6
- data/lib/legion/tty/screens/chat.rb +20 -2
- data/lib/legion/tty/screens/onboarding.rb +69 -0
- data/lib/legion/tty/version.rb +1 -1
- metadata +30 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f5fae7ebc821ab002ba505d7570c4b3116cd8cb9ebe2fe663aa63e93c5e459e
|
|
4
|
+
data.tar.gz: 127da7a51afc0eb408b63dbc32c6c0df7971fc767938a5e2d6ac6c435e6742ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97c6604bd14b08983fb7f189be0150e111b69d59b5d55ce0768f72b5fa8612f596979979aed0e2c93a93461c56cfbcb9b37bf1938b8f6a9c41054470331acbcb
|
|
7
|
+
data.tar.gz: 36630d28f144a4c15fa08ac9a93f476161be4dc6dc42bc0045cabef31a5e1cfeb11ec01e80875cfb4d628735951bc42d90203959505faed4cf44b03429291bae
|
data/CHANGELOG.md
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/legion/tty/version.rb
CHANGED
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.
|
|
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
|