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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 209050c59aa0893b1c8eb87a31a17af8c162857f6ed45d87bca6ed3e575d15c1
4
- data.tar.gz: e35271f8f8bfa9fae282d46a12e561406a16ffa3aacdd3ab214c626c7f3f91a0
3
+ metadata.gz: d25022bbf25ee324c11c0517e9186a65e13b4838d33dde2dc4250b9803783631
4
+ data.tar.gz: 1831cd59d1263dd3099854dc58cc5ecfb73f1448ac180cac73271ab836541fb6
5
5
  SHA512:
6
- metadata.gz: d3d9daa8c885f9f117fa0e1d9a36a14c3e603927be0058f382fdc2551fbe99f6143a0f81d3316884ba5a227e0a716db479240ce50b2c429861c967d0356a3b54
7
- data.tar.gz: 9dd409dc9e0bfea51c99a29720b00d5727b8e8e189bffb33ab5d92c574283a8b72461e445f7c52df27d8737cd3f31c4666e87055a605b60ed69891131dd1eda4
6
+ metadata.gz: c03d4a80b353b12ab4e28cdddd4aed2c4a3645cbbb8f9d96d9636d6e889873f509ec3ca8dc813013ebbed1f8d38bdb4c6f41cc554998a0fcbfbd27f167e6e5eb
7
+ data.tar.gz: e3dc1f1741c4bbb83a406164034788564ce6d5a1c45a286702b2a36470997789ed0a41632d02c467b12ac88dd4f279b4f613b6bddeb4a9aecd49b3de58b540a0
@@ -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
- # Rebind Tab (^I = byte 9) to cycle agents when the buffer is empty,
79
- # otherwise fall through to normal completion. We define a custom method
80
- # on the singleton LineEditor instance and point the emacs keymap at it.
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
- provider = Brute.provider rescue nil
143
- @provider_name = provider&.name&.to_s
144
- @model_name = @selected_model || provider&.default_model&.to_s
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
- idx = (AGENTS.index(@current_agent) + 1) % AGENTS.size
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 "Model: #{@model_name.colorize(ACCENT)}"
318
- puts separator
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(ACCENT)
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(ACCENT)
788
+ ("─" * [@width, 40].max).colorize(DIM)
711
789
  end
712
790
 
713
791
  def thick_separator
714
- ("═" * [@width, 40].max).colorize(ACCENT)
792
+ ("═" * [@width, 40].max).colorize(DIM)
715
793
  end
716
794
 
717
795
  def detect_width
@@ -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 = :light_black
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 }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BruteCli
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/brute_cli.rb CHANGED
@@ -28,7 +28,8 @@ module BruteCLI
28
28
  888 888 888 888 888 888 . 888 .o
29
29
  `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
30
30
  LOGO
31
-
31
+
32
+ # yolo, bruv...
32
33
 
33
34
  def self.error(message)
34
35
  $stderr.puts "#{"ERROR".colorize(ERROR_BG)} #{message.colorize(ERROR_FG)}"
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.2.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-01 00:00:00.000000000 Z
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.1.9
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.1.9
25
+ version: 0.2.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: brute_flow
28
28
  requirement: !ruby/object:Gem::Requirement