brute_cli 0.2.0 → 0.3.0
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/lib/brute_cli/repl.rb +100 -22
- data/lib/brute_cli/styles.rb +1 -1
- data/lib/brute_cli/version.rb +1 -1
- data/lib/brute_cli.rb +2 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d25022bbf25ee324c11c0517e9186a65e13b4838d33dde2dc4250b9803783631
|
|
4
|
+
data.tar.gz: 1831cd59d1263dd3099854dc58cc5ecfb73f1448ac180cac73271ab836541fb6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c03d4a80b353b12ab4e28cdddd4aed2c4a3645cbbb8f9d96d9636d6e889873f509ec3ca8dc813013ebbed1f8d38bdb4c6f41cc554998a0fcbfbd27f167e6e5eb
|
|
7
|
+
data.tar.gz: e3dc1f1741c4bbb83a406164034788564ce6d5a1c45a286702b2a36470997789ed0a41632d02c467b12ac88dd4f279b4f613b6bddeb4a9aecd49b3de58b540a0
|
data/lib/brute_cli/repl.rb
CHANGED
|
@@ -11,7 +11,16 @@ require "brute_cli/question_screen"
|
|
|
11
11
|
|
|
12
12
|
module BruteCLI
|
|
13
13
|
class REPL
|
|
14
|
-
AGENTS = %w[build plan].freeze
|
|
14
|
+
AGENTS = %w[build plan bash ruby python nix].freeze
|
|
15
|
+
|
|
16
|
+
# Shell-mode agents: agent name → shell interpreter (model name).
|
|
17
|
+
# These agents use the Shell provider instead of the current LLM provider.
|
|
18
|
+
SHELL_AGENTS = {
|
|
19
|
+
"bash" => "bash",
|
|
20
|
+
"ruby" => "ruby",
|
|
21
|
+
"python" => "python",
|
|
22
|
+
"nix" => "nix",
|
|
23
|
+
}.freeze
|
|
15
24
|
|
|
16
25
|
def initialize(options = {})
|
|
17
26
|
@options = options
|
|
@@ -20,10 +29,12 @@ module BruteCLI
|
|
|
20
29
|
@session = nil
|
|
21
30
|
@selected_model = nil # user-chosen model override (nil = provider default)
|
|
22
31
|
@models_cache = nil # cached model list from provider API
|
|
32
|
+
@saved_provider = nil # stashed LLM provider when in shell-mode agent
|
|
23
33
|
@width = TTY::Screen.width
|
|
24
34
|
@content_buf = +""
|
|
25
35
|
@streamer = StreamFormatter.new(width: @width)
|
|
26
36
|
@spinner = nil
|
|
37
|
+
@last_output = nil # :separator, :content, or :tool — used to deduplicate separators
|
|
27
38
|
@mu = Mutex.new
|
|
28
39
|
end
|
|
29
40
|
|
|
@@ -75,23 +86,38 @@ module BruteCLI
|
|
|
75
86
|
end
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# on the
|
|
89
|
+
# Force Reline's config to load now (reads inputrc, registers ANSI
|
|
90
|
+
# default key bindings). Without this, the defaults are lazily
|
|
91
|
+
# applied on the first readmultiline call and overwrite our overrides.
|
|
92
|
+
unless Reline.core.config.loaded?
|
|
93
|
+
Reline.core.config.read
|
|
94
|
+
Reline::IOGate.set_default_key_bindings(Reline.core.config)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Rebind Tab (^I = byte 9) to cycle agents forward when the buffer is
|
|
98
|
+
# empty, otherwise fall through to normal completion.
|
|
99
|
+
# Shift+Tab (ESC [ Z = bytes 27,91,90) cycles agents backward.
|
|
81
100
|
repl = self
|
|
82
101
|
Reline.line_editor.define_singleton_method(:cycle_or_complete) do |key|
|
|
83
102
|
if current_line.empty?
|
|
84
|
-
repl.send(:cycle_agent)
|
|
85
|
-
# Reline caches prompt_list based on (whole_lines, mode_string).
|
|
86
|
-
# Since neither changed, the cache returns the stale prompt.
|
|
87
|
-
# Clear it so the next rerender re-evaluates prompt_proc.
|
|
103
|
+
repl.send(:cycle_agent, :forward)
|
|
88
104
|
@cache.delete(:prompt_list)
|
|
89
105
|
@cache.delete(:wrapped_prompt_and_input_lines)
|
|
90
106
|
else
|
|
91
107
|
complete(key)
|
|
92
108
|
end
|
|
93
109
|
end
|
|
110
|
+
|
|
111
|
+
Reline.line_editor.define_singleton_method(:reverse_cycle_agent) do |_key|
|
|
112
|
+
if current_line.empty?
|
|
113
|
+
repl.send(:cycle_agent, :backward)
|
|
114
|
+
@cache.delete(:prompt_list)
|
|
115
|
+
@cache.delete(:wrapped_prompt_and_input_lines)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
94
119
|
Reline.core.config.add_default_key_binding_by_keymap(:emacs, [9], :cycle_or_complete)
|
|
120
|
+
Reline.core.config.add_default_key_binding_by_keymap(:emacs, [27, 91, 90], :reverse_cycle_agent)
|
|
95
121
|
end
|
|
96
122
|
|
|
97
123
|
# Reline completion callback.
|
|
@@ -139,9 +165,14 @@ module BruteCLI
|
|
|
139
165
|
# ── Provider ──
|
|
140
166
|
|
|
141
167
|
def resolve_provider_info
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
168
|
+
if (shell_model = SHELL_AGENTS[@current_agent])
|
|
169
|
+
@provider_name = "shell"
|
|
170
|
+
@model_name = shell_model
|
|
171
|
+
else
|
|
172
|
+
provider = Brute.provider rescue nil
|
|
173
|
+
@provider_name = provider&.name&.to_s
|
|
174
|
+
@model_name = @selected_model || provider&.default_model&.to_s
|
|
175
|
+
end
|
|
145
176
|
end
|
|
146
177
|
|
|
147
178
|
def model_short
|
|
@@ -170,6 +201,14 @@ module BruteCLI
|
|
|
170
201
|
return if @agent
|
|
171
202
|
|
|
172
203
|
ensure_session!
|
|
204
|
+
|
|
205
|
+
# Shell-mode agents swap the provider to Shell with the right interpreter.
|
|
206
|
+
if (shell_model = SHELL_AGENTS[@current_agent])
|
|
207
|
+
activate_shell_agent!(shell_model)
|
|
208
|
+
else
|
|
209
|
+
restore_llm_provider!
|
|
210
|
+
end
|
|
211
|
+
|
|
173
212
|
@agent = Brute.agent(
|
|
174
213
|
cwd: @options[:cwd] || Dir.pwd,
|
|
175
214
|
model: @selected_model,
|
|
@@ -185,6 +224,26 @@ module BruteCLI
|
|
|
185
224
|
@session.restore(@agent.context) if @options[:session_id]
|
|
186
225
|
end
|
|
187
226
|
|
|
227
|
+
# Swap the global provider to Shell with the given interpreter model.
|
|
228
|
+
# Saves the current LLM provider so it can be restored later.
|
|
229
|
+
def activate_shell_agent!(shell_model)
|
|
230
|
+
current = Brute.provider
|
|
231
|
+
unless current.is_a?(Brute::Providers::Shell)
|
|
232
|
+
@saved_provider = current
|
|
233
|
+
end
|
|
234
|
+
Brute.provider = Brute::Providers::Shell.new
|
|
235
|
+
@selected_model = shell_model
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Restore the saved LLM provider when leaving a shell-mode agent.
|
|
239
|
+
def restore_llm_provider!
|
|
240
|
+
if @saved_provider
|
|
241
|
+
Brute.provider = @saved_provider
|
|
242
|
+
@saved_provider = nil
|
|
243
|
+
@selected_model = nil
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
188
247
|
# Force the agent to be recreated on next ensure_agent! call.
|
|
189
248
|
# Used after changing provider, model, or agent.
|
|
190
249
|
def reset_agent!
|
|
@@ -196,10 +255,25 @@ module BruteCLI
|
|
|
196
255
|
"%"
|
|
197
256
|
end
|
|
198
257
|
|
|
199
|
-
def cycle_agent
|
|
200
|
-
|
|
258
|
+
def cycle_agent(direction = :forward)
|
|
259
|
+
step = direction == :backward ? -1 : 1
|
|
260
|
+
idx = (AGENTS.index(@current_agent) + step) % AGENTS.size
|
|
201
261
|
@current_agent = AGENTS[idx]
|
|
202
262
|
reset_agent!
|
|
263
|
+
|
|
264
|
+
# Pre-resolve provider info for the status line.
|
|
265
|
+
# Shell agents show "shell" provider + interpreter model;
|
|
266
|
+
# LLM agents show the current LLM provider + model.
|
|
267
|
+
if (shell_model = SHELL_AGENTS[@current_agent])
|
|
268
|
+
@provider_name = "shell"
|
|
269
|
+
@model_name = shell_model
|
|
270
|
+
else
|
|
271
|
+
# Peek at what the LLM provider will be (saved or current).
|
|
272
|
+
provider = @saved_provider || (Brute.provider rescue nil)
|
|
273
|
+
@provider_name = provider&.name&.to_s
|
|
274
|
+
@model_name = @selected_model || provider&.default_model&.to_s
|
|
275
|
+
end
|
|
276
|
+
|
|
203
277
|
# Rewrite the model/status line sitting one line above the prompt.
|
|
204
278
|
# Save cursor, move up, clear line, print, restore cursor.
|
|
205
279
|
parts = []
|
|
@@ -314,8 +388,8 @@ module BruteCLI
|
|
|
314
388
|
resolve_provider_info
|
|
315
389
|
puts separator
|
|
316
390
|
puts "Provider changed to: #{provider_name.colorize(ACCENT)}"
|
|
317
|
-
puts "
|
|
318
|
-
|
|
391
|
+
puts "Select a model:".colorize(DIM)
|
|
392
|
+
cmd_model
|
|
319
393
|
else
|
|
320
394
|
puts "Failed to initialize provider: #{provider_name}".colorize(ERROR_FG)
|
|
321
395
|
end
|
|
@@ -392,6 +466,7 @@ module BruteCLI
|
|
|
392
466
|
def execute(prompt)
|
|
393
467
|
@content_buf = +""
|
|
394
468
|
@streamer.reset
|
|
469
|
+
@last_output = nil
|
|
395
470
|
|
|
396
471
|
start_spinner("Thinking...")
|
|
397
472
|
|
|
@@ -442,7 +517,8 @@ module BruteCLI
|
|
|
442
517
|
|
|
443
518
|
def start_spinner(label)
|
|
444
519
|
stop_spinner
|
|
445
|
-
puts separator
|
|
520
|
+
puts separator unless @last_output == :separator
|
|
521
|
+
@last_output = :separator
|
|
446
522
|
@spinner = TTY::Spinner.new(
|
|
447
523
|
":spinner #{label}",
|
|
448
524
|
frames: nyan_frames,
|
|
@@ -469,6 +545,7 @@ module BruteCLI
|
|
|
469
545
|
stop_spinner
|
|
470
546
|
@content_buf << text
|
|
471
547
|
@streamer << text
|
|
548
|
+
@last_output = :content
|
|
472
549
|
end
|
|
473
550
|
end
|
|
474
551
|
|
|
@@ -502,8 +579,9 @@ module BruteCLI
|
|
|
502
579
|
stop_spinner
|
|
503
580
|
tool = @pending_tool || { name: name, args: {} }
|
|
504
581
|
|
|
505
|
-
puts separator
|
|
582
|
+
puts separator unless @last_output == :separator
|
|
506
583
|
print_tool_result(tool, result)
|
|
584
|
+
@last_output = :tool
|
|
507
585
|
|
|
508
586
|
@pending_tool = nil
|
|
509
587
|
start_spinner("Thinking...")
|
|
@@ -530,6 +608,7 @@ module BruteCLI
|
|
|
530
608
|
unless @content_buf.strip.empty?
|
|
531
609
|
@streamer.flush
|
|
532
610
|
@content_buf = +""
|
|
611
|
+
@last_output = :content
|
|
533
612
|
end
|
|
534
613
|
end
|
|
535
614
|
|
|
@@ -638,8 +717,7 @@ module BruteCLI
|
|
|
638
717
|
parts << stat_span("out", (tokens[:total_output] || 0).to_s)
|
|
639
718
|
parts << stat_span("time", format_time(timing[:total_elapsed] || 0))
|
|
640
719
|
parts << stat_span("tools", tool_calls.to_s) if tool_calls > 0
|
|
641
|
-
puts
|
|
642
|
-
puts separator
|
|
720
|
+
puts separator unless @last_output == :separator
|
|
643
721
|
puts parts.join(sep)
|
|
644
722
|
puts thick_separator
|
|
645
723
|
end
|
|
@@ -660,7 +738,7 @@ module BruteCLI
|
|
|
660
738
|
|
|
661
739
|
def print_banner
|
|
662
740
|
puts separator
|
|
663
|
-
puts BruteCLI::LOGO.chomp.colorize(
|
|
741
|
+
puts BruteCLI::LOGO.chomp.colorize(DIM)
|
|
664
742
|
puts separator
|
|
665
743
|
puts "Version #{Brute::VERSION}".colorize(DIM)
|
|
666
744
|
if @session
|
|
@@ -707,11 +785,11 @@ module BruteCLI
|
|
|
707
785
|
end
|
|
708
786
|
|
|
709
787
|
def separator
|
|
710
|
-
("─" * [@width, 40].max).colorize(
|
|
788
|
+
("─" * [@width, 40].max).colorize(DIM)
|
|
711
789
|
end
|
|
712
790
|
|
|
713
791
|
def thick_separator
|
|
714
|
-
("═" * [@width, 40].max).colorize(
|
|
792
|
+
("═" * [@width, 40].max).colorize(DIM)
|
|
715
793
|
end
|
|
716
794
|
|
|
717
795
|
def detect_width
|
data/lib/brute_cli/styles.rb
CHANGED
|
@@ -22,7 +22,7 @@ module BruteCLI
|
|
|
22
22
|
|
|
23
23
|
# Named color/style constants for use with "string".colorize(CONST).
|
|
24
24
|
# Compound styles (bold + color, background + foreground) use the hash form.
|
|
25
|
-
DIM = :
|
|
25
|
+
DIM = :grey
|
|
26
26
|
ACCENT = COLOR
|
|
27
27
|
ACCENT_BOLD = { color: COLOR, mode: :bold }
|
|
28
28
|
ACCENT_BG = { color: :black, background: COLOR, mode: :bold }
|
data/lib/brute_cli/version.rb
CHANGED
data/lib/brute_cli.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brute_cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 1980-01-
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: brute
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.2.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: brute_flow
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|