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.
- checksums.yaml +4 -4
- data/exe/brute +3 -0
- data/lib/brute_cli/bat.rb +67 -0
- data/lib/brute_cli/commands.rb +38 -0
- data/lib/brute_cli/emoji.rb +2 -0
- data/lib/brute_cli/fzf_menu.rb +150 -0
- data/lib/brute_cli/question_screen.rb +334 -0
- data/lib/brute_cli/repl.rb +496 -194
- data/lib/brute_cli/stream_formatter.rb +114 -0
- data/lib/brute_cli/styles.rb +36 -35
- data/lib/brute_cli/version.rb +1 -1
- data/lib/brute_cli.rb +18 -5
- metadata +94 -5
|
@@ -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
|
data/lib/brute_cli/styles.rb
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "colorize"
|
|
4
4
|
|
|
5
5
|
module BruteCLI
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
Lipgloss::Style.new.respond_to?(name) || super
|
|
14
|
-
end
|
|
18
|
+
DEFAULT_THEME = "lemon-and-lime"
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
data/lib/brute_cli/version.rb
CHANGED
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
|
-
|
|
34
|
+
$stderr.puts "#{"ERROR".colorize(ERROR_BG)} #{message.colorize(ERROR_FG)}"
|
|
22
35
|
end
|
|
23
36
|
|
|
24
37
|
def self.warn(message)
|
|
25
|
-
|
|
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.
|
|
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-
|
|
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:
|
|
55
|
+
name: colorize
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
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: '
|
|
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:
|