brute_cli 0.3.0 → 0.4.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/exe/brute +11 -26
  3. data/lib/brute_cli/buffer_output/error.rb +27 -0
  4. data/lib/brute_cli/buffer_output/model_line.rb +37 -0
  5. data/lib/brute_cli/buffer_output/separator.rb +24 -0
  6. data/lib/brute_cli/buffer_output/stats_bar.rb +43 -0
  7. data/lib/brute_cli/buffer_output.rb +11 -0
  8. data/lib/brute_cli/configuration.rb +14 -0
  9. data/lib/brute_cli/emoji.rb +25 -22
  10. data/lib/brute_cli/execution.rb +260 -0
  11. data/lib/brute_cli/phase/content_phase.rb +29 -0
  12. data/lib/brute_cli/phase/tool_call.rb +21 -0
  13. data/lib/brute_cli/phase/tool_phase.rb +29 -0
  14. data/lib/brute_cli/phase.rb +10 -0
  15. data/lib/brute_cli/repl.rb +99 -444
  16. data/lib/brute_cli/spinner/dots.rb +24 -0
  17. data/lib/brute_cli/spinner/nyan.rb +53 -0
  18. data/lib/brute_cli/spinner/puff_puff_pass.rb +32 -0
  19. data/lib/brute_cli/spinner.rb +30 -0
  20. data/lib/brute_cli/styles.rb +3 -0
  21. data/lib/brute_cli/tool_output/delegate.rb +16 -0
  22. data/lib/brute_cli/tool_output/fetch.rb +16 -0
  23. data/lib/brute_cli/tool_output/fs_search.rb +16 -0
  24. data/lib/brute_cli/tool_output/patch.rb +20 -0
  25. data/lib/brute_cli/tool_output/question.rb +9 -0
  26. data/lib/brute_cli/tool_output/read.rb +16 -0
  27. data/lib/brute_cli/tool_output/remove.rb +16 -0
  28. data/lib/brute_cli/tool_output/shell.rb +25 -0
  29. data/lib/brute_cli/tool_output/todo_read.rb +16 -0
  30. data/lib/brute_cli/tool_output/todo_write.rb +16 -0
  31. data/lib/brute_cli/tool_output/undo.rb +16 -0
  32. data/lib/brute_cli/tool_output/write.rb +20 -0
  33. data/lib/brute_cli/tool_output.rb +141 -0
  34. data/lib/brute_cli/version.rb +1 -1
  35. data/lib/brute_cli.rb +15 -12
  36. metadata +32 -4
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-spinner"
4
+
5
+ module BruteCLI
6
+ module Spinner
7
+ class Nyan < Base
8
+ RAINBOW = [
9
+ "\e[38;2;255;56;96m", "\e[38;2;255;165;0m",
10
+ "\e[38;2;255;220;0m", "\e[38;2;0;219;68m",
11
+ "\e[38;2;0;186;255m", "\e[38;2;107;80;255m",
12
+ "\e[38;2;255;96;255m",
13
+ ].freeze
14
+ RESET = "\e[0m"
15
+
16
+ def start
17
+ stop if spinning?
18
+ @tty = TTY::Spinner.new(
19
+ ":spinner #{label}",
20
+ frames: frames,
21
+ interval: interval,
22
+ output: $stdout,
23
+ clear: true,
24
+ )
25
+ @tty.auto_spin
26
+ end
27
+
28
+ def stop
29
+ @tty&.stop
30
+ @tty = nil
31
+ end
32
+
33
+ def spinning?
34
+ @tty&.spinning? || false
35
+ end
36
+
37
+ def label
38
+ "Thinking..."
39
+ end
40
+
41
+ def frames
42
+ bar = "━" * 12
43
+ bar.length.times.map do |offset|
44
+ bar.chars.map.with_index { |c, i| RAINBOW[(i + offset) % RAINBOW.length] + c }.join + RESET
45
+ end
46
+ end
47
+
48
+ def interval
49
+ 8
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-spinner"
4
+
5
+ module BruteCLI
6
+ module Spinner
7
+ class PuffPuffPass < Base
8
+ FRAMES = (1..4).map { |n| (Emoji::SMOKE + " ") * n }.freeze
9
+
10
+ def start
11
+ stop if spinning?
12
+ @tty = TTY::Spinner.new(
13
+ ":spinner",
14
+ frames: FRAMES,
15
+ interval: 200,
16
+ output: $stdout,
17
+ clear: true,
18
+ )
19
+ @tty.auto_spin
20
+ end
21
+
22
+ def stop
23
+ @tty&.stop
24
+ @tty = nil
25
+ end
26
+
27
+ def spinning?
28
+ @tty&.spinning? || false
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module Spinner
5
+ # Abstract base class for CLI spinners.
6
+ #
7
+ # Subclasses must implement:
8
+ # #start – begin the spinner animation
9
+ # #stop – halt the spinner animation
10
+ # #spinning? – whether the spinner is currently animating
11
+ #
12
+ class Base
13
+ def start
14
+ raise NotImplementedError, "#{self.class.name} must implement #start"
15
+ end
16
+
17
+ def stop
18
+ raise NotImplementedError, "#{self.class.name} must implement #stop"
19
+ end
20
+
21
+ def spinning?
22
+ raise NotImplementedError, "#{self.class.name} must implement #spinning?"
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ require "brute_cli/spinner/nyan"
29
+ require "brute_cli/spinner/dots"
30
+ require "brute_cli/spinner/puff_puff_pass"
@@ -33,6 +33,9 @@ module BruteCLI
33
33
  # Re-derive all accent constants from a new color.
34
34
  # Call this before any output (e.g. right after option parsing).
35
35
  def self.apply_theme!(name)
36
+
37
+ # honestly what the fuck is this shit???????????
38
+
36
39
  color = THEMES.fetch(name) do
37
40
  raise ArgumentError, "Unknown theme #{name.inspect}. Valid themes: #{THEMES.keys.join(', ')}"
38
41
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Delegate < Base
6
+ ICON = Emoji::ROBOT
7
+
8
+ private
9
+
10
+ def summary
11
+ task = arg(:task)
12
+ task ? task.to_s[0..60].colorize(DIM) : ""
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Fetch < Base
6
+ ICON = Emoji::GLOBE
7
+
8
+ private
9
+
10
+ def summary
11
+ url = arg(:url)
12
+ url ? url.to_s.colorize(DIM) : ""
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class FsSearch < Base
6
+ ICON = Emoji::MAG
7
+
8
+ private
9
+
10
+ def summary
11
+ pattern = arg(:pattern)
12
+ pattern ? "\"#{pattern}\"".colorize(DIM) : ""
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Patch < Base
6
+ ICON = Emoji::HAMMER
7
+
8
+ private
9
+
10
+ def summary
11
+ path = arg(:file_path)
12
+ path ? path.to_s.colorize(DIM) : ""
13
+ end
14
+
15
+ def body_lines
16
+ diff_lines
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Question < Base
6
+ ICON = Emoji::DIAMOND
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Read < Base
6
+ ICON = Emoji::EYES
7
+
8
+ private
9
+
10
+ def summary
11
+ path = arg(:file_path)
12
+ path ? path.to_s.colorize(DIM) : ""
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Remove < Base
6
+ ICON = Emoji::WASTEBASKET
7
+
8
+ private
9
+
10
+ def summary
11
+ path = arg(:path)
12
+ path ? path.to_s.colorize(DIM) : ""
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Shell < Base
6
+ ICON = Emoji::COMPUTER
7
+
8
+ private
9
+
10
+ def summary
11
+ cmd = arg(:command)
12
+ cmd ? cmd.to_s[0..60].colorize(DIM) : ""
13
+ end
14
+
15
+ def body_lines
16
+ stdout = result_val(:stdout)
17
+ return [] unless stdout && !stdout.to_s.strip.empty?
18
+
19
+ lines = stdout.strip.lines.map(&:chomp)
20
+ lines = lines.first(15) + ["... (truncated)"] if lines.size > 15
21
+ lines.map { |l| l.colorize(DIM) }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class TodoRead < Base
6
+ ICON = Emoji::CLIPBOARD
7
+
8
+ private
9
+
10
+ def body_lines
11
+ todos = result_val(:todos)
12
+ todo_lines(todos)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class TodoWrite < Base
6
+ ICON = Emoji::CLIPBOARD
7
+
8
+ private
9
+
10
+ def body_lines
11
+ todos = arg(:todos)
12
+ todo_lines(todos)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Undo < Base
6
+ ICON = Emoji::REWIND
7
+
8
+ private
9
+
10
+ def summary
11
+ path = arg(:path)
12
+ path ? path.to_s.colorize(DIM) : ""
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Write < Base
6
+ ICON = Emoji::WRITING
7
+
8
+ private
9
+
10
+ def summary
11
+ path = arg(:file_path)
12
+ path ? path.to_s.colorize(DIM) : ""
13
+ end
14
+
15
+ def body_lines
16
+ diff_lines
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BruteCLI
4
+ module ToolOutput
5
+ class Base
6
+ include BruteCLI
7
+
8
+ ICON = Emoji::GEAR
9
+
10
+ TODO_STATUS = {
11
+ "pending" => Emoji::SQUARE,
12
+ "in_progress" => Emoji::ARROWS,
13
+ "completed" => Emoji::CHECK,
14
+ "cancelled" => Emoji::CROSS,
15
+ }.freeze
16
+
17
+ def initialize(tool_call, width:)
18
+ @call = tool_call
19
+ @width = width
20
+ @args = normalize_args(@call.arguments)
21
+ @result = @call.result
22
+ end
23
+
24
+ def to_s
25
+ lines = [header_line]
26
+ if @call.resolved?
27
+ lines.concat(body_lines)
28
+ lines.concat(error_lines) if error?
29
+ end
30
+ lines.join("\n")
31
+ end
32
+
33
+ private
34
+
35
+ def icon = self.class::ICON
36
+ def name = @call.name.to_s
37
+
38
+ def header_line
39
+ "#{icon} #{name.colorize(ACCENT_BG)} #{summary}".rstrip
40
+ end
41
+
42
+ def summary = ""
43
+
44
+ def body_lines
45
+ [] # subclasses override
46
+ end
47
+
48
+ # ── Shared helpers ──
49
+
50
+ def arg(key)
51
+ @args[key.to_s] || @args[key.to_sym]
52
+ end
53
+
54
+ def result_val(key)
55
+ @result.is_a?(Hash) && (@result[key.to_s] || @result[key.to_sym])
56
+ end
57
+
58
+ def error?
59
+ @result.is_a?(Hash) && (@result[:error] || @result["error"])
60
+ end
61
+
62
+ def error_lines
63
+ msg = error_message
64
+ msg = msg[0..70] + "..." if msg.length > 70
65
+ ["#{"FAILED".colorize(ERROR_BG)} #{msg.colorize(DIM)}"]
66
+ end
67
+
68
+ def error_message
69
+ if @result.is_a?(Hash)
70
+ (
71
+ @result[:message] ||
72
+ @result["message"] ||
73
+ @result[:error] ||
74
+ @result["error"]
75
+ ).to_s
76
+ else
77
+ ""
78
+ end
79
+ end
80
+
81
+ def diff_lines
82
+ diff = result_val(:diff)
83
+ if diff && !diff.to_s.strip.empty?
84
+ [BruteCLI::Bat.diff_mode(diff, width: @width).chomp]
85
+ else
86
+ []
87
+ end
88
+ end
89
+
90
+ def todo_lines(todos)
91
+ return [] unless todos && !todos.empty?
92
+
93
+ todos.map do |t|
94
+ t = t.transform_keys(&:to_s) if t.is_a?(Hash)
95
+ status = t["status"].to_s
96
+ ico = TODO_STATUS[status] || Emoji::SQUARE
97
+ content = t["content"] || t["id"] || "?"
98
+ " #{ico} #{content}"
99
+ end
100
+ end
101
+
102
+ def normalize_args(args)
103
+ args.is_a?(Hash) ? args : {}
104
+ end
105
+ end
106
+
107
+ require_relative "tool_output/read"
108
+ require_relative "tool_output/write"
109
+ require_relative "tool_output/patch"
110
+ require_relative "tool_output/shell"
111
+ require_relative "tool_output/fs_search"
112
+ require_relative "tool_output/fetch"
113
+ require_relative "tool_output/remove"
114
+ require_relative "tool_output/undo"
115
+ require_relative "tool_output/delegate"
116
+ require_relative "tool_output/question"
117
+ require_relative "tool_output/todo_read"
118
+ require_relative "tool_output/todo_write"
119
+
120
+ MAP = {
121
+ "read" => Read,
122
+ "write" => Write,
123
+ "patch" => Patch,
124
+ "shell" => Shell,
125
+ "fs_search" => FsSearch,
126
+ "fetch" => Fetch,
127
+ "remove" => Remove,
128
+ "undo" => Undo,
129
+ "delegate" => Delegate,
130
+ "question" => Question,
131
+ "todo_read" => TodoRead,
132
+ "todo_write" => TodoWrite,
133
+ }.freeze
134
+
135
+ # Returns a ToolOutput instance for the given ToolCall.
136
+ def self.for(tool_call, width: 80)
137
+ klass = MAP[tool_call.name.to_s] || Base
138
+ klass.new(tool_call, width: width)
139
+ end
140
+ end
141
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BruteCli
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/brute_cli.rb CHANGED
@@ -2,13 +2,12 @@
2
2
 
3
3
  require 'brute'
4
4
 
5
- begin
5
+ begin # why?
6
6
  require 'brute_flow'
7
7
  rescue LoadError
8
8
  end
9
9
 
10
10
  require 'tty-screen'
11
- require 'tty-spinner'
12
11
  require 'brute_cli/version'
13
12
  require 'brute_cli/styles'
14
13
  require 'brute_cli/emoji'
@@ -16,20 +15,24 @@ require 'brute_cli/bat'
16
15
  require 'brute_cli/stream_formatter'
17
16
  require 'brute_cli/fzf_menu'
18
17
  require 'brute_cli/commands'
18
+ require 'brute_cli/spinner'
19
+ require 'brute_cli/configuration'
20
+ require 'brute_cli/buffer_output'
21
+ require 'brute_cli/execution'
19
22
  require 'brute_cli/repl'
20
23
 
21
24
  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
25
+ MONIKER = <<~MONIKER
26
+ .o8 .
27
+ "888 .o8
28
+ 888oooo. oooo d8b oooo oooo .o888oo .ooooo.
29
+ d88' `88b `888""8P `888 `888 888 d88' `88b
30
+ 888 888 888 888 888 888 888ooo888
31
+ 888 888 888 888 888 888 . 888 .o
32
+ `Y8bod8P' d888b `V88V"V8P' "888" `Y8bod8P'
33
+ MONIKER
31
34
 
32
- # yolo, bruv...
35
+ # museum of modern art, bruv...
33
36
 
34
37
  def self.error(message)
35
38
  $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.3.0
4
+ version: 0.4.0
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
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.2.0
18
+ version: 0.4.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.2.0
25
+ version: 0.4.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: brute_flow
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -187,13 +187,41 @@ files:
187
187
  - exe/brute
188
188
  - lib/brute_cli.rb
189
189
  - lib/brute_cli/bat.rb
190
+ - lib/brute_cli/buffer_output.rb
191
+ - lib/brute_cli/buffer_output/error.rb
192
+ - lib/brute_cli/buffer_output/model_line.rb
193
+ - lib/brute_cli/buffer_output/separator.rb
194
+ - lib/brute_cli/buffer_output/stats_bar.rb
190
195
  - lib/brute_cli/commands.rb
196
+ - lib/brute_cli/configuration.rb
191
197
  - lib/brute_cli/emoji.rb
198
+ - lib/brute_cli/execution.rb
192
199
  - lib/brute_cli/fzf_menu.rb
200
+ - lib/brute_cli/phase.rb
201
+ - lib/brute_cli/phase/content_phase.rb
202
+ - lib/brute_cli/phase/tool_call.rb
203
+ - lib/brute_cli/phase/tool_phase.rb
193
204
  - lib/brute_cli/question_screen.rb
194
205
  - lib/brute_cli/repl.rb
206
+ - lib/brute_cli/spinner.rb
207
+ - lib/brute_cli/spinner/dots.rb
208
+ - lib/brute_cli/spinner/nyan.rb
209
+ - lib/brute_cli/spinner/puff_puff_pass.rb
195
210
  - lib/brute_cli/stream_formatter.rb
196
211
  - lib/brute_cli/styles.rb
212
+ - lib/brute_cli/tool_output.rb
213
+ - lib/brute_cli/tool_output/delegate.rb
214
+ - lib/brute_cli/tool_output/fetch.rb
215
+ - lib/brute_cli/tool_output/fs_search.rb
216
+ - lib/brute_cli/tool_output/patch.rb
217
+ - lib/brute_cli/tool_output/question.rb
218
+ - lib/brute_cli/tool_output/read.rb
219
+ - lib/brute_cli/tool_output/remove.rb
220
+ - lib/brute_cli/tool_output/shell.rb
221
+ - lib/brute_cli/tool_output/todo_read.rb
222
+ - lib/brute_cli/tool_output/todo_write.rb
223
+ - lib/brute_cli/tool_output/undo.rb
224
+ - lib/brute_cli/tool_output/write.rb
197
225
  - lib/brute_cli/version.rb
198
226
  licenses:
199
227
  - MIT