brute_cli 0.1.2 → 0.1.3

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.
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-markdown"
4
+
5
+ module BruteCLI
6
+ # Streaming markdown renderer powered by TTY::Markdown.
7
+ #
8
+ # Tokens arrive one-at-a-time from the LLM via SSE. The StreamFormatter
9
+ # accumulates them into a buffer, printing raw characters for
10
+ # immediate feedback. On each newline, the entire buffer is
11
+ # re-rendered through TTY::Markdown.parse and the previous output
12
+ # is overwritten via ANSI cursor save/restore.
13
+ #
14
+ # Uses DEC save/restore cursor (\e7 / \e8) — the terminal
15
+ # tracks its own position, so we don't need fragile manual
16
+ # row/column accounting.
17
+ #
18
+ # fmt = BruteCLI::StreamFormatter.new(width: 80)
19
+ # streamer << "# He" # prints raw "# He"
20
+ # streamer << "llo World\n" # re-renders as styled "Hello World"
21
+ # streamer.flush # finalize any partial line
22
+ # streamer.reset # ready for next turn
23
+ #
24
+ class StreamFormatter
25
+ SAVE_CURSOR = "\e7"
26
+ RESTORE_CURSOR = "\e8"
27
+ CLEAR_TO_END = "\e[J"
28
+
29
+ def initialize(output: nil, width: nil)
30
+ @output = output
31
+ @width = width || TTY::Screen.width
32
+ reset
33
+ end
34
+
35
+ # Accept a chunk of streamed text. May contain zero, one, or many
36
+ # newlines — each is handled correctly.
37
+ def <<(text)
38
+ text.each_char { |ch| consume(ch) }
39
+ end
40
+
41
+ # Finalize any partial (unterminated) line still in the buffer.
42
+ # Called when the response ends or a tool call interrupts.
43
+ #
44
+ # After flushing, all state is cleared so the next block of
45
+ # content (after tool frames, etc.) starts fresh.
46
+ def flush
47
+ return if @buffer.empty? && @line_buf.empty?
48
+ @buffer << @line_buf unless @line_buf.empty?
49
+ restore_and_render
50
+ out.puts
51
+ @buffer = +""
52
+ @line_buf = +""
53
+ @origin_saved = false
54
+ end
55
+
56
+ # Reset all state for the next agent turn.
57
+ def reset
58
+ @buffer = +""
59
+ @line_buf = +""
60
+ @origin_saved = false
61
+ end
62
+
63
+ private
64
+
65
+ # Process a single character.
66
+ def consume(ch)
67
+ save_origin unless @origin_saved
68
+ @line_buf << ch
69
+ if ch == "\n"
70
+ finish_line
71
+ else
72
+ out.print ch
73
+ end
74
+ end
75
+
76
+ # Save the cursor position at the start of this output block.
77
+ # Called once before the first character is printed, then not
78
+ # again until after a flush or reset.
79
+ def save_origin
80
+ out.print SAVE_CURSOR
81
+ @origin_saved = true
82
+ end
83
+
84
+ # A complete line arrived — append to the buffer and re-render.
85
+ def finish_line
86
+ @buffer << @line_buf
87
+ @line_buf = +""
88
+ restore_and_render
89
+ end
90
+
91
+ # Restore cursor to the saved origin, clear everything after it,
92
+ # and print the fully rendered markdown.
93
+ def restore_and_render
94
+ rendered = render_markdown(@buffer)
95
+ out.print RESTORE_CURSOR
96
+ out.print CLEAR_TO_END
97
+ out.print rendered
98
+ end
99
+
100
+ def render_markdown(text)
101
+ TTY::Markdown.parse(text, width: @width, color: :always)
102
+ rescue => _e
103
+ # If TTY::Markdown chokes on partial markdown, return raw text
104
+ text
105
+ end
106
+
107
+ # Resolve the output stream. When no explicit output was provided,
108
+ # use $stdout dynamically so that test helpers like capture_stdout
109
+ # (which swap $stdout) work transparently.
110
+ def out
111
+ @output || $stdout
112
+ end
113
+ end
114
+ end
@@ -1,45 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'lipgloss'
3
+ require "colorize"
4
4
 
5
5
  module BruteCLI
6
- module Styles
7
- # Styles.foreground("#fff").bold(true) etc -- delegates to Lipgloss::Style.new
8
- def self.method_missing(name, *args, &block)
9
- Lipgloss::Style.new.send(name, *args, &block)
10
- end
6
+ # Available themes — each maps to a colorize color symbol.
7
+ THEMES = {
8
+ "lemon-and-lime" => :yellow,
9
+ "ganja-king" => :green,
10
+ "og-treacle" => :magenta,
11
+ "blues-clues" => :blue,
12
+ "hot-cheeto" => :light_red,
13
+ "blue-ice-vape" => :light_magenta,
14
+ "matrix" => :light_green,
15
+ "mrs-jackson" => :red,
16
+ }.freeze
11
17
 
12
- def self.respond_to_missing?(name, include_private = false)
13
- Lipgloss::Style.new.respond_to?(name) || super
14
- end
18
+ DEFAULT_THEME = "lemon-and-lime"
15
19
 
16
- # Brand colors
17
- PURPLE = '#6B50FF'
18
- PINK = '#FF60FF'
19
- CYAN = '#3EEFCF'
20
- RED = '#FF5F87'
21
- DIM = '#757575'
22
- MUTED = '#585858'
23
- WHITE = '#F1F1F1'
24
- DARK_BG = '#1A1A2E'
20
+ # Global accent color — change this one constant to re-theme the entire CLI.
21
+ COLOR = THEMES.fetch(DEFAULT_THEME)
25
22
 
26
- PROMPT = foreground(PURPLE).bold(true)
27
- DIM_TEXT = foreground(DIM)
28
- SEPARATOR = foreground(MUTED)
29
- STAT_VALUE = foreground(CYAN)
30
- ERROR_BADGE = foreground(WHITE).background(RED).bold(true).padding_left(1).padding_right(1)
31
- ERROR_REASON = foreground(RED)
32
- TOOL_BADGE = foreground(DARK_BG).background(CYAN).bold(true).padding_left(1).padding_right(1)
33
- TOOL_ARG_KEY = foreground(PURPLE)
34
- TOOL_ARG_VAL = foreground(DIM)
35
- TOOL_OK = foreground(DARK_BG).background(CYAN).padding_left(1).padding_right(1)
36
- TOOL_FAIL = foreground(WHITE).background(RED).bold(true).padding_left(1).padding_right(1)
37
- TOOL_FRAME = border_style(:rounded).border_foreground(MUTED).padding_left(1).padding_right(1)
23
+ # Named color/style constants for use with "string".colorize(CONST).
24
+ # Compound styles (bold + color, background + foreground) use the hash form.
25
+ DIM = :light_black
26
+ ACCENT = COLOR
27
+ ACCENT_BOLD = { color: COLOR, mode: :bold }
28
+ ACCENT_BG = { color: :black, background: COLOR, mode: :bold }
29
+ ACCENT_BG2 = { color: :black, background: COLOR }
30
+ ERROR_BG = { color: :white, background: :red, mode: :bold }
31
+ ERROR_FG = :red
32
+
33
+ # Re-derive all accent constants from a new color.
34
+ # Call this before any output (e.g. right after option parsing).
35
+ def self.apply_theme!(name)
36
+ color = THEMES.fetch(name) do
37
+ raise ArgumentError, "Unknown theme #{name.inspect}. Valid themes: #{THEMES.keys.join(', ')}"
38
+ end
38
39
 
39
- DIFF_ADDED = foreground('#4fd6be')
40
- DIFF_REMOVED = foreground('#c53b53')
41
- DIFF_HUNK = foreground(CYAN).bold(true)
42
- DIFF_CONTEXT = foreground(DIM)
43
- TOOL_INLINE = foreground(DIM)
40
+ remove_const(:COLOR) ; const_set(:COLOR, color)
41
+ remove_const(:ACCENT) ; const_set(:ACCENT, color)
42
+ remove_const(:ACCENT_BOLD) ; const_set(:ACCENT_BOLD, { color: color, mode: :bold })
43
+ remove_const(:ACCENT_BG) ; const_set(:ACCENT_BG, { color: :black, background: color, mode: :bold })
44
+ remove_const(:ACCENT_BG2) ; const_set(:ACCENT_BG2, { color: :black, background: color })
44
45
  end
45
46
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BruteCli
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/brute_cli.rb CHANGED
@@ -7,21 +7,34 @@ begin
7
7
  rescue LoadError
8
8
  end
9
9
 
10
+ require 'tty-screen'
10
11
  require 'tty-spinner'
11
- require 'lipgloss'
12
- require 'glamour'
13
-
14
12
  require 'brute_cli/version'
15
13
  require 'brute_cli/styles'
16
14
  require 'brute_cli/emoji'
15
+ require 'brute_cli/bat'
16
+ require 'brute_cli/stream_formatter'
17
+ require 'brute_cli/fzf_menu'
18
+ require 'brute_cli/commands'
17
19
  require 'brute_cli/repl'
18
20
 
19
21
  module BruteCLI
22
+ LOGO = <<-LOGO
23
+ .o8 .
24
+ "888 .o8
25
+ 888oooo. oooo d8b oooo oooo .o888oo .ooooo.
26
+ d88' `88b `888""8P `888 `888 888 d88' `88b
27
+ 888 888 888 888 888 888 888ooo888
28
+ 888 888 888 888 888 888 . 888 .o
29
+ `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
30
+ LOGO
31
+
32
+
20
33
  def self.error(message)
21
- warn "#{Styles::ERROR_BADGE.render('ERROR')} #{Styles::ERROR_REASON.render(message)}"
34
+ $stderr.puts "#{"ERROR".colorize(ERROR_BG)} #{message.colorize(ERROR_FG)}"
22
35
  end
23
36
 
24
37
  def self.warn(message)
25
- warn Styles::DIM_TEXT.render("warning: #{message}")
38
+ $stderr.puts "warning: #{message}".colorize(DIM)
26
39
  end
27
40
  end
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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: brute
@@ -52,19 +52,75 @@ dependencies:
52
52
  - !ruby/object:Gem::Version
53
53
  version: '4.1'
54
54
  - !ruby/object:Gem::Dependency
55
- name: glamour
55
+ name: colorize
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '0.2'
60
+ version: '1.1'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: '0.2'
67
+ version: '1.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: reline
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0.5'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0.5'
82
+ - !ruby/object:Gem::Dependency
83
+ name: tty-markdown
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.7'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.7'
96
+ - !ruby/object:Gem::Dependency
97
+ name: tty-screen
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.8'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.8'
110
+ - !ruby/object:Gem::Dependency
111
+ name: bubbletea
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.1'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.1'
68
124
  - !ruby/object:Gem::Dependency
69
125
  name: lipgloss
70
126
  requirement: !ruby/object:Gem::Requirement
@@ -79,6 +135,20 @@ dependencies:
79
135
  - - "~>"
80
136
  - !ruby/object:Gem::Version
81
137
  version: '0.2'
138
+ - !ruby/object:Gem::Dependency
139
+ name: bubbles
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.1'
145
+ type: :runtime
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '0.1'
82
152
  - !ruby/object:Gem::Dependency
83
153
  name: tty-spinner
84
154
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +163,20 @@ dependencies:
93
163
  - - "~>"
94
164
  - !ruby/object:Gem::Version
95
165
  version: '0.9'
166
+ - !ruby/object:Gem::Dependency
167
+ name: rspec
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '3.13'
173
+ type: :development
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '3.13'
96
180
  description: Interactive command-line interface for the Brute coding agent. Supports
97
181
  single-prompt, interactive, piped, and session modes.
98
182
  executables:
@@ -102,8 +186,13 @@ extra_rdoc_files: []
102
186
  files:
103
187
  - exe/brute
104
188
  - lib/brute_cli.rb
189
+ - lib/brute_cli/bat.rb
190
+ - lib/brute_cli/commands.rb
105
191
  - lib/brute_cli/emoji.rb
192
+ - lib/brute_cli/fzf_menu.rb
193
+ - lib/brute_cli/question_screen.rb
106
194
  - lib/brute_cli/repl.rb
195
+ - lib/brute_cli/stream_formatter.rb
107
196
  - lib/brute_cli/styles.rb
108
197
  - lib/brute_cli/version.rb
109
198
  licenses: