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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +1 -1
- data/lib/legion/tty/app.rb +23 -9
- data/lib/legion/tty/background/github_probe.rb +4 -2
- data/lib/legion/tty/background/llm_probe.rb +1 -0
- data/lib/legion/tty/background/scanner.rb +20 -9
- data/lib/legion/tty/components/command_palette.rb +2 -1
- data/lib/legion/tty/components/digital_rain.rb +2 -1
- data/lib/legion/tty/components/input_bar.rb +2 -1
- data/lib/legion/tty/components/markdown_view.rb +1 -0
- data/lib/legion/tty/components/message_stream.rb +4 -2
- data/lib/legion/tty/components/model_picker.rb +2 -1
- data/lib/legion/tty/components/progress_panel.rb +2 -1
- data/lib/legion/tty/components/session_picker.rb +2 -1
- data/lib/legion/tty/components/table_view.rb +1 -0
- data/lib/legion/tty/components/tool_call_parser.rb +114 -0
- data/lib/legion/tty/daemon_client.rb +16 -11
- data/lib/legion/tty/screens/chat/export_commands.rb +5 -1
- data/lib/legion/tty/screens/chat/message_commands.rb +7 -3
- data/lib/legion/tty/screens/chat/model_commands.rb +3 -1
- data/lib/legion/tty/screens/chat/session_commands.rb +4 -2
- data/lib/legion/tty/screens/chat/ui_commands.rb +9 -3
- data/lib/legion/tty/screens/chat.rb +35 -9
- data/lib/legion/tty/screens/config.rb +5 -2
- data/lib/legion/tty/screens/dashboard.rb +16 -9
- data/lib/legion/tty/screens/extensions.rb +2 -1
- data/lib/legion/tty/screens/onboarding.rb +91 -8
- data/lib/legion/tty/session_store.rb +4 -2
- 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: f37085f25b964517a88988f6780ae8d60257356d53d3021567a4f9f0bc43f400
|
|
4
|
+
data.tar.gz: d1b150a3993d20e92eb30d778bbd4fe79c75582ec3ad893668879d5abc70ae76
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
data/lib/legion/tty/app.rb
CHANGED
|
@@ -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
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
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.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
|