brute_cli 0.1.1 → 0.1.2

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: 8505051f8c9895144293784e61c82810556a9a9e82896a7808a69c6ea28df7f1
4
- data.tar.gz: f17b5dbe8a96c4a0c8960f9e871fce0190c54a06d1e52610ee2b1448c7dfaea0
3
+ metadata.gz: 5d935729b509640bc8f34ab0250ba7aa4760c66f600e2f8ce4dc74ad2e9e756a
4
+ data.tar.gz: 7b61d6668976f55bfd334d718b6beb274ffb29a6cd85de1cd4178b7da59e211b
5
5
  SHA512:
6
- metadata.gz: 1ac361628483b1be32ce71fd2d4a140df73f4813be3636edc00e59ea4ad78fbf6fedb86c38083b3e28de39e517895fd300ffafe8fb9134ba6f822068bfb28f41
7
- data.tar.gz: 80cbf082aceb6af2aeff93dbd589f56dc9722151bd8ff82abe68f071bea46360fa5ada02eaa9df50de9b38a5b7d19525bafe9eb6f8b163eabb7da7369dec4732
6
+ metadata.gz: 750488466360f678c3ec34a637980dbb727e64f8b24cd27e86f015503a14b9dedd36da7eca002f905f4e7ddd191b42a9fe2d163bad229c83765d2a26de9f8ecf
7
+ data.tar.gz: 7917a11383995c907541543b4893f2067ab50ae16389b56054255e866a3f35f7b223e3b37d69c4b4b97876e245c9ef1bb843567598cf05024b497fde2fe43880
data/exe/brute CHANGED
@@ -15,73 +15,50 @@ OptionParser.new do |opts|
15
15
  opts.on("-h", "--help", "Show help") { puts opts; exit }
16
16
  end.parse!
17
17
 
18
+ # ── List sessions ──
19
+
18
20
  if options[:list]
19
21
  sessions = Brute::Session.list
20
22
  if sessions.empty?
21
23
  puts "No saved sessions."
22
24
  else
23
- sessions.each { |s| puts " #{s[:id]} #{s[:title] || "(untitled)"} #{s[:saved_at]}" }
25
+ title_style = Lipgloss::Style.new.bold(true).foreground(BruteCLI::Styles::PURPLE)
26
+ id_style = Lipgloss::Style.new.foreground(BruteCLI::Styles::CYAN)
27
+ time_style = Lipgloss::Style.new.foreground(BruteCLI::Styles::DIM)
28
+
29
+ sessions.each do |s|
30
+ id = id_style.render(s[:id][0..7])
31
+ title = title_style.render(s[:title] || "(untitled)")
32
+ time = time_style.render(s[:saved_at].to_s)
33
+ puts " #{id} #{title} #{time}"
34
+ end
24
35
  end
25
36
  exit
26
37
  end
27
38
 
28
- # Agent is created lazily on first prompt — no API key needed to boot.
29
- session = Brute::Session.new(id: options[:session_id])
30
- orch = nil
31
-
32
- make_agent = -> {
33
- return orch if orch
34
- orch = Brute.agent(
35
- cwd: options[:cwd],
36
- session: session,
37
- on_content: ->(text) { print text },
38
- on_reasoning: ->(_text) { },
39
- on_tool_call: ->(name, args) {
40
- $stderr.puts "\n--- [tool] #{name} ---"
41
- if args.is_a?(Hash)
42
- args.each { |k, v| $stderr.puts " #{k}: #{v.to_s[0..100]}" }
43
- end
44
- },
45
- on_tool_result: ->(name, result) {
46
- if result.is_a?(Hash) && result[:error]
47
- $stderr.puts " [FAILED] #{result[:error]}"
48
- else
49
- $stderr.puts " [ok]"
50
- end
51
- },
52
- )
53
- session.restore(orch.context) if options[:session_id]
54
- orch
55
- }
39
+ # ── Collect prompt ──
56
40
 
57
41
  prompt = ARGV.join(" ")
58
42
  prompt = $stdin.read.strip if prompt.empty? && !$stdin.tty?
59
43
 
44
+ # ── Run ──
45
+
46
+ repl = BruteCLI::REPL.new(options)
47
+
60
48
  begin
61
49
  if prompt.empty?
62
- $stderr.puts "brute> (interactive mode, Ctrl-D to exit)\n\n"
63
- loop do
64
- $stderr.print "brute> "
65
- input = $stdin.gets&.chomp
66
- break if input.nil? || input.strip == "exit"
67
- next if input.strip.empty?
68
- puts
69
- make_agent.call.run(input)
70
- puts "\n\n"
71
- end
50
+ # Interactive mode
51
+ repl.run_interactive
72
52
  else
73
- make_agent.call.run(prompt)
53
+ # Single prompt mode
54
+ repl.run_once(prompt)
74
55
  puts
75
56
  end
76
57
  rescue Interrupt
77
58
  $stderr.puts "\nAborted."
78
59
  exit 130
79
- rescue RuntimeError, LLM::RateLimitError, LLM::ServerError => e
80
- BruteCLI.error(e.message)
81
- $stderr.puts " #{e.backtrace.first}".red if ENV["BRUTE_DEBUG"]
82
- exit 1
83
60
  rescue => e
84
61
  BruteCLI.error(e.message)
85
- $stderr.puts " #{e.backtrace.first}".red if ENV["BRUTE_DEBUG"]
62
+ $stderr.puts " #{e.backtrace&.first}" if ENV["BRUTE_DEBUG"]
86
63
  exit 1
87
64
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gemoji'
4
+
5
+ module BruteCLI
6
+ module Emoji
7
+ def self.find(name)
8
+ ::Emoji.find_by_alias(name)&.raw || ''
9
+ end
10
+
11
+ EYES = find('eyes')
12
+ PENCIL = find('pencil2')
13
+ PAGE = find('page_facing_up')
14
+ COMPUTER = find('computer')
15
+ SPARKLES = find('sparkles')
16
+ GLOBE = find('globe_with_meridians')
17
+ WASTEBASKET = find('wastebasket')
18
+ REWIND = find('rewind')
19
+ DIAMOND = find('diamond_shape_with_a_dot_inside')
20
+ GEAR = find('gear')
21
+ MAG = find('mag')
22
+ HAMMER = find('hammer_and_wrench')
23
+ PACKAGE = find('package')
24
+ CLIPBOARD = find('clipboard')
25
+ CHECK = find('white_check_mark')
26
+ CROSS = find('x')
27
+ WRITING = find('writing_hand')
28
+ ROBOT = find('robot')
29
+ FOLDER = find('file_folder')
30
+ end
31
+ end
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'io/console'
5
+ require 'reline'
6
+ require 'tty-spinner'
7
+ require 'brute_cli/styles'
8
+
9
+ module BruteCLI
10
+ class REPL
11
+ def initialize(options = {})
12
+ @options = options
13
+ @agent = nil
14
+ @session = nil
15
+ @width = detect_width
16
+ @content_buf = +''
17
+ @spinner = nil
18
+ end
19
+
20
+ def run_once(prompt)
21
+ ensure_agent!
22
+ execute(prompt)
23
+ end
24
+
25
+ def run_interactive
26
+ print_banner
27
+ resolve_provider_info
28
+ setup_reline
29
+
30
+ loop do
31
+ result = read_prompt
32
+ break if result.nil?
33
+ next if result.empty?
34
+ break if %w[exit quit].include?(result)
35
+
36
+ ensure_agent!
37
+ execute(result)
38
+ $stdout.puts
39
+ end
40
+ rescue Interrupt
41
+ $stdout.puts
42
+ end
43
+
44
+ private
45
+
46
+ # ── Reline ──
47
+
48
+ def setup_reline
49
+ subtitle = build_subtitle
50
+
51
+ Reline.prompt_proc = proc { |lines|
52
+ lines.map.with_index do |_, i|
53
+ i == 0 ? Styles::PROMPT.render('>') + ' ' : Styles::DIM_TEXT.render('.') + ' '
54
+ end
55
+ }
56
+
57
+ Reline.add_dialog_proc(:brute_status, lambda {
58
+ Reline::DialogRenderInfo.new(
59
+ pos: Reline::CursorPos.new(0, cursor_pos.y > 0 ? 3 : 1),
60
+ contents: [subtitle],
61
+ width: screen_width
62
+ )
63
+ }, nil)
64
+ end
65
+
66
+ def read_prompt
67
+ input = Reline.readmultiline(Styles::PROMPT.render('>') + ' ', true) { |t| !t.rstrip.end_with?('\\') }
68
+ return nil if input.nil?
69
+
70
+ input.gsub(/\\\n/, "\n").strip
71
+ end
72
+
73
+ # ── Provider ──
74
+
75
+ def resolve_provider_info
76
+ provider = begin
77
+ Brute.provider
78
+ rescue StandardError
79
+ nil
80
+ end
81
+ @provider_name = provider&.name&.to_s
82
+ @model_name = provider&.default_model&.to_s
83
+ end
84
+
85
+ def model_short
86
+ @model_name&.sub(/^claude-/, '')&.sub(/-\d{8}$/, '') || @model_name
87
+ end
88
+
89
+ def build_subtitle
90
+ parts = []
91
+ parts << stat_span(@provider_name, model_short) if @provider_name && model_short
92
+ parts << stat_span('agent', 'brute')
93
+ ' ' + parts.join(Styles::DIM_TEXT.render(' · '))
94
+ end
95
+
96
+ def stat_span(label, value)
97
+ Styles::DIM_TEXT.render("#{label} ") + Styles::STAT_VALUE.render(value)
98
+ end
99
+
100
+ # ── Agent ──
101
+
102
+ def ensure_agent!
103
+ return if @agent
104
+
105
+ @session = Brute::Session.new(id: @options[:session_id])
106
+ @agent = Brute.agent(
107
+ cwd: @options[:cwd] || Dir.pwd,
108
+ session: @session,
109
+ logger: Logger.new(File::NULL),
110
+ on_content: method(:on_content),
111
+ on_reasoning: method(:on_reasoning),
112
+ on_tool_call: method(:on_tool_call),
113
+ on_tool_result: method(:on_tool_result)
114
+ )
115
+ @session.restore(@agent.context) if @options[:session_id]
116
+ end
117
+
118
+ # ── Execute ──
119
+
120
+ def execute(prompt)
121
+ @content_buf = +''
122
+
123
+ print_model_line
124
+ start_spinner('Thinking...')
125
+
126
+ begin
127
+ @agent.run(prompt)
128
+ rescue Interrupt
129
+ stop_spinner
130
+ flush_content
131
+ styled_puts Styles::DIM_TEXT.render(' Aborted.')
132
+ print_stats_bar
133
+ return
134
+ rescue StandardError => e
135
+ stop_spinner
136
+ flush_content
137
+ print_error(e)
138
+ print_stats_bar
139
+ return
140
+ end
141
+
142
+ stop_spinner
143
+ flush_content
144
+ print_stats_bar
145
+ end
146
+
147
+ def print_model_line
148
+ parts = []
149
+ parts << stat_span(@provider_name, model_short) if @provider_name && model_short
150
+ parts << stat_span('agent', 'brute')
151
+ styled_puts ' ' + parts.join(Styles::DIM_TEXT.render(' · '))
152
+ end
153
+
154
+ # ── Spinner ──
155
+
156
+ RAINBOW = [
157
+ "\e[38;2;255;56;96m", "\e[38;2;255;165;0m",
158
+ "\e[38;2;255;220;0m", "\e[38;2;0;219;68m",
159
+ "\e[38;2;0;186;255m", "\e[38;2;107;80;255m",
160
+ "\e[38;2;255;96;255m"
161
+ ].freeze
162
+ RESET = "\e[0m"
163
+
164
+ def nyan_frames
165
+ bar = '━' * 12
166
+ bar.length.times.map do |offset|
167
+ bar.chars.map.with_index { |c, i| RAINBOW[(i + offset) % RAINBOW.length] + c }.join + RESET
168
+ end
169
+ end
170
+
171
+ def start_spinner(label)
172
+ stop_spinner
173
+ @spinner = TTY::Spinner.new(
174
+ " :spinner #{label}",
175
+ frames: nyan_frames,
176
+ interval: 8,
177
+ output: $stdout
178
+ )
179
+ @spinner.auto_spin
180
+ end
181
+
182
+ def stop_spinner
183
+ return unless @spinner
184
+
185
+ @spinner.stop('') if @spinner.spinning?
186
+ @spinner = nil
187
+ end
188
+
189
+ # ── Callbacks ──
190
+
191
+ def on_content(text)
192
+ stop_spinner
193
+ @content_buf << text
194
+ end
195
+
196
+ def on_reasoning(_text); end
197
+
198
+ INLINE_TOOLS = %w[read fs_search todo_read todo_write fetch].freeze
199
+ TOOL_ICONS = {
200
+ 'read' => Emoji::EYES, 'patch' => Emoji::HAMMER, 'write' => Emoji::WRITING,
201
+ 'shell' => Emoji::COMPUTER, 'fs_search' => Emoji::MAG, 'fetch' => Emoji::GLOBE,
202
+ 'todo_read' => Emoji::CLIPBOARD, 'todo_write' => Emoji::CLIPBOARD,
203
+ 'remove' => Emoji::WASTEBASKET, 'undo' => Emoji::REWIND, 'delegate' => Emoji::ROBOT
204
+ }.freeze
205
+
206
+ def on_tool_call(name, args)
207
+ stop_spinner
208
+ flush_content
209
+ @pending_tool = { name: name, args: args }
210
+ end
211
+
212
+ def on_tool_result(name, result)
213
+ stop_spinner
214
+ tool = @pending_tool || { name: name, args: {} }
215
+
216
+ if INLINE_TOOLS.include?(tool[:name])
217
+ print_inline_tool(tool, result)
218
+ else
219
+ print_block_tool(tool, result)
220
+ end
221
+
222
+ @pending_tool = nil
223
+ start_spinner('Thinking...')
224
+ end
225
+
226
+ # ── Output ──
227
+
228
+ def flush_content
229
+ return if @content_buf.strip.empty?
230
+
231
+ rendered = glamour_render(@content_buf)
232
+ $stdout.puts
233
+ $stdout.puts rendered
234
+ $stdout.flush
235
+ @content_buf = +''
236
+ end
237
+
238
+ def glamour_render(text)
239
+ width = [@width - 4, 40].max
240
+ Glamour.render(text.strip, style: 'auto', width: width).rstrip
241
+ rescue StandardError => _e
242
+ text
243
+ end
244
+
245
+ def print_inline_tool(tool, result)
246
+ icon = TOOL_ICONS[tool[:name].to_s] || Emoji::GEAR
247
+ name = tool[:name].to_s
248
+ summary = tool_summary(tool) || ''
249
+
250
+ if error_result?(result)
251
+ styled_puts " #{icon} #{Styles::TOOL_BADGE.render(name)} #{summary} #{Styles::TOOL_FAIL.render('FAILED')}"
252
+ else
253
+ styled_puts " #{icon} #{Styles::TOOL_BADGE.render(name)} #{summary}"
254
+ end
255
+ end
256
+
257
+ def print_block_tool(tool, result)
258
+ icon = TOOL_ICONS[tool[:name].to_s] || Emoji::GEAR
259
+ name = tool[:name].to_s
260
+ title = "#{icon} #{Styles::TOOL_BADGE.render(name)}"
261
+
262
+ body_lines = []
263
+
264
+ summary = tool_summary(tool) || ''
265
+ body_lines << summary unless summary.empty?
266
+
267
+ # Diff
268
+ diff = result.is_a?(Hash) && (result[:diff] || result['diff'])
269
+ body_lines.concat(format_diff_lines(diff)) if diff && !diff.strip.empty?
270
+
271
+ # Shell output
272
+ stdout = result.is_a?(Hash) && (result[:stdout] || result['stdout'])
273
+ if stdout && !stdout.strip.empty?
274
+ lines = stdout.strip.lines.map(&:chomp)
275
+ lines = lines.first(15) + [Styles::DIM_TEXT.render('... (truncated)')] if lines.size > 15
276
+ body_lines.concat(lines.map { |l| Styles::DIM_TEXT.render(l) })
277
+ end
278
+
279
+ # Status
280
+ if error_result?(result)
281
+ msg = error_message(result)
282
+ msg = msg[0..70] + '...' if msg.length > 70
283
+ body_lines << "#{Styles::TOOL_FAIL.render('FAILED')} #{Styles::DIM_TEXT.render(msg)}"
284
+ else
285
+ body_lines << Styles::TOOL_OK.render('OK')
286
+ end
287
+
288
+ styled_puts render_titled_frame(title, body_lines)
289
+ end
290
+
291
+ def error_result?(result)
292
+ return false unless result.is_a?(Hash)
293
+
294
+ result[:error] || result['error']
295
+ end
296
+
297
+ def error_message(result)
298
+ return '' unless result.is_a?(Hash)
299
+
300
+ (result[:message] || result['message'] || result[:error] || result['error']).to_s
301
+ end
302
+
303
+ def tool_summary(tool)
304
+ args = tool[:args]
305
+ return '' unless args.is_a?(Hash) && !args.empty?
306
+
307
+ # Show the most relevant arg (file_path, command, etc.)
308
+ path = args['file_path'] || args[:file_path]
309
+ cmd = args['command'] || args[:command]
310
+ pattern = args['pattern'] || args[:pattern]
311
+
312
+ if path
313
+ Styles::DIM_TEXT.render(path.to_s)
314
+ elsif cmd
315
+ Styles::DIM_TEXT.render(cmd.to_s[0..60])
316
+ elsif pattern
317
+ Styles::DIM_TEXT.render("\"#{pattern}\"")
318
+ else
319
+ ''
320
+ end
321
+ end
322
+
323
+ def format_diff_lines(diff_text)
324
+ diff_text.lines.map do |line|
325
+ l = line.chomp
326
+ case l[0]
327
+ when '+' then Styles::DIFF_ADDED.render(l)
328
+ when '-' then Styles::DIFF_REMOVED.render(l)
329
+ when '@' then Styles::DIFF_HUNK.render(l)
330
+ else Styles::DIFF_CONTEXT.render(l)
331
+ end
332
+ end
333
+ end
334
+
335
+ def render_titled_frame(title, body_lines)
336
+ m = Styles::SEPARATOR
337
+ title_w = visible_width(title)
338
+ body_w = body_lines.map { |l| visible_width(l) }.max || 0
339
+ inner_w = [title_w, body_w].max + 2
340
+
341
+ top = m.render('╭─ ') + title + ' ' + m.render('─' * [inner_w - title_w - 1, 0].max + '╮')
342
+ bot = m.render('╰' + '─' * (inner_w + 2) + '╯')
343
+ mid = body_lines.map do |l|
344
+ pad = inner_w - visible_width(l)
345
+ m.render('│') + ' ' + l + ' ' * [pad, 0].max + ' ' + m.render('│')
346
+ end
347
+
348
+ ([top] + mid + [bot]).join("\n")
349
+ end
350
+
351
+ def visible_width(str)
352
+ str.gsub(/\e\[[0-9;]*m/, '').gsub(/\p{Emoji_Presentation}|\p{Emoji}\uFE0F?/, 'XX').length
353
+ end
354
+
355
+ # ── Stats ──
356
+
357
+ def print_stats_bar
358
+ metadata = @agent&.env&.dig(:metadata) || {}
359
+ tokens = metadata[:tokens] || {}
360
+ timing = metadata[:timing] || {}
361
+ tool_calls = metadata[:tool_calls] || 0
362
+ parts = []
363
+ parts << format_stat('tokens', format_tokens(tokens))
364
+ parts << format_stat('time', format_time(timing[:total_elapsed] || 0))
365
+ parts << format_stat('tools', tool_calls.to_s) if tool_calls > 0
366
+ $stdout.puts
367
+ styled_puts separator
368
+ styled_puts ' ' + parts.join(Styles::DIM_TEXT.render(' | '))
369
+ styled_puts separator
370
+ end
371
+
372
+ def format_stat(l, v)
373
+ Styles::DIM_TEXT.render("#{l} ") + Styles::STAT_VALUE.render(v)
374
+ end
375
+
376
+ def format_tokens(t)
377
+ total = t[:total] || 0
378
+ return '0' if total == 0
379
+
380
+ "#{total} (#{t[:total_input] || 0}in/#{t[:total_output] || 0}out)"
381
+ end
382
+
383
+ def format_time(s)
384
+ s < 60 ? "#{s.round(1)}s" : "#{(s / 60).floor}m#{(s % 60).round(1)}s"
385
+ end
386
+
387
+ # ── Error ──
388
+
389
+ def print_error(err)
390
+ styled_puts "\n#{Styles::ERROR_BADGE.render('ERROR')} #{Styles::ERROR_REASON.render(err.message)}"
391
+ styled_puts Styles::DIM_TEXT.render(" #{err.backtrace.first}") if ENV['BRUTE_DEBUG'] && err.backtrace&.first
392
+ end
393
+
394
+ # ── UI ──
395
+
396
+ def print_banner
397
+ styled_puts separator
398
+ styled_puts Styles::DIM_TEXT.render(" brute #{Brute::VERSION} — interactive mode")
399
+ styled_puts separator
400
+ $stdout.puts
401
+ end
402
+
403
+ def styled_puts(text)
404
+ $stdout.puts text
405
+ $stdout.flush
406
+ end
407
+
408
+ def separator
409
+ Styles::SEPARATOR.render('─' * [@width, 40].max)
410
+ end
411
+
412
+ def detect_width
413
+ _rows, cols = IO.console&.winsize
414
+ cols || 80
415
+ rescue StandardError
416
+ 80
417
+ end
418
+ end
419
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lipgloss'
4
+
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
11
+
12
+ def self.respond_to_missing?(name, include_private = false)
13
+ Lipgloss::Style.new.respond_to?(name) || super
14
+ end
15
+
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'
25
+
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)
38
+
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)
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BruteCli
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/brute_cli.rb CHANGED
@@ -1,29 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "brute"
4
- require "colorize"
5
- require "emoji"
3
+ require 'brute'
6
4
 
7
- # Optionally load brute_flow if available.
8
5
  begin
9
- require "brute_flow"
6
+ require 'brute_flow'
10
7
  rescue LoadError
11
- # brute_flow is optional for CLI usage
12
8
  end
13
9
 
14
- module BruteCLI
15
- VERSION = "0.1.1"
10
+ require 'tty-spinner'
11
+ require 'lipgloss'
12
+ require 'glamour'
16
13
 
17
- CROSS_MARK = Emoji.find_by_alias("x").raw
18
- WARNING_SIGN = Emoji.find_by_alias("warning").raw
14
+ require 'brute_cli/version'
15
+ require 'brute_cli/styles'
16
+ require 'brute_cli/emoji'
17
+ require 'brute_cli/repl'
19
18
 
20
- # Print a red error message with a cross mark prefix to stderr.
19
+ module BruteCLI
21
20
  def self.error(message)
22
- $stderr.puts "#{CROSS_MARK} #{message}".red
21
+ warn "#{Styles::ERROR_BADGE.render('ERROR')} #{Styles::ERROR_REASON.render(message)}"
23
22
  end
24
23
 
25
- # Print a yellow warning to stderr.
26
24
  def self.warn(message)
27
- $stderr.puts "#{WARNING_SIGN} #{message}".yellow
25
+ warn Styles::DIM_TEXT.render("warning: #{message}")
28
26
  end
29
27
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brute_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors
@@ -38,33 +38,61 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.1'
40
40
  - !ruby/object:Gem::Dependency
41
- name: colorize
41
+ name: gemoji
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '1.1'
46
+ version: '4.1'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '1.1'
53
+ version: '4.1'
54
54
  - !ruby/object:Gem::Dependency
55
- name: gemoji
55
+ name: glamour
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: '4.1'
60
+ version: '0.2'
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: '4.1'
67
+ version: '0.2'
68
+ - !ruby/object:Gem::Dependency
69
+ name: lipgloss
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.2'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.2'
82
+ - !ruby/object:Gem::Dependency
83
+ name: tty-spinner
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.9'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.9'
68
96
  description: Interactive command-line interface for the Brute coding agent. Supports
69
97
  single-prompt, interactive, piped, and session modes.
70
98
  executables:
@@ -74,6 +102,9 @@ extra_rdoc_files: []
74
102
  files:
75
103
  - exe/brute
76
104
  - lib/brute_cli.rb
105
+ - lib/brute_cli/emoji.rb
106
+ - lib/brute_cli/repl.rb
107
+ - lib/brute_cli/styles.rb
77
108
  - lib/brute_cli/version.rb
78
109
  licenses:
79
110
  - MIT