openclacky 0.5.1 → 0.5.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.
@@ -8,7 +8,8 @@ module Clacky
8
8
  :on_tool_error,
9
9
  :on_start,
10
10
  :on_complete,
11
- :on_iteration
11
+ :on_iteration,
12
+ :session_rollback
12
13
  ].freeze
13
14
 
14
15
  def initialize
@@ -14,7 +14,9 @@ module Clacky
14
14
  def start
15
15
  @start_time = Time.now
16
16
  @running = true
17
- print_status("#{@thinking_verb}… (ctrl+c to interrupt) ")
17
+ # Save cursor position after the [..] symbol
18
+ print "\e[s" # Save cursor position
19
+ print_thinking_status("#{@thinking_verb}… (ctrl+c to interrupt)")
18
20
 
19
21
  # Start background thread to update elapsed time
20
22
  @update_thread = Thread.new do
@@ -29,24 +31,24 @@ module Clacky
29
31
  return unless @start_time
30
32
 
31
33
  elapsed = (Time.now - @start_time).to_i
32
- print_status("#{@thinking_verb}… (ctrl+c to interrupt · #{elapsed}s) ")
34
+ print_thinking_status("#{@thinking_verb}… (ctrl+c to interrupt · #{elapsed}s)")
33
35
  end
34
36
 
35
37
  def finish
36
38
  @running = false
37
39
  @update_thread&.join
38
- clear_line
40
+ # Restore cursor and clear to end of line
41
+ print "\e[u" # Restore cursor position
42
+ print "\e[K" # Clear to end of line
43
+ puts "" # Add newline after finishing
39
44
  end
40
45
 
41
46
  private
42
47
 
43
- def print_status(text)
44
- print "\r\033[K#{text}" # \r moves to start of line, \033[K clears to end of line
45
- $stdout.flush
46
- end
47
-
48
- def clear_line
49
- print "\r\033[K" # Clear the entire line
48
+ def print_thinking_status(text)
49
+ print "\e[u" # Restore cursor position (to after [..] symbol)
50
+ print "\e[K" # Clear to end of line from cursor
51
+ print text
50
52
  $stdout.flush
51
53
  end
52
54
  end
@@ -147,6 +147,12 @@ module Clacky
147
147
  trash_directory = Clacky::TrashDirectory.new(@project_root)
148
148
  @trash_dir = trash_directory.trash_dir
149
149
  @backup_dir = trash_directory.backup_dir
150
+
151
+ # Setup safety log directory under ~/.clacky/safety_logs/
152
+ @project_hash = trash_directory.generate_project_hash(@project_root)
153
+ @safety_log_dir = File.join(Dir.home, ".clacky", "safety_logs", @project_hash)
154
+ FileUtils.mkdir_p(@safety_log_dir) unless Dir.exist?(@safety_log_dir)
155
+ @safety_log_file = File.join(@safety_log_dir, "safety.log")
150
156
  end
151
157
 
152
158
  def make_command_safe(command)
@@ -384,8 +390,7 @@ module Clacky
384
390
  end
385
391
 
386
392
  def write_log(log_entry)
387
- log_file = File.join(@project_root, '.ai_safety.log')
388
- File.open(log_file, 'a') do |f|
393
+ File.open(@safety_log_file, 'a') do |f|
389
394
  f.puts JSON.generate(log_entry)
390
395
  end
391
396
  rescue StandardError
@@ -260,7 +260,7 @@ module Clacky
260
260
  - Use 'list' to see what files are in trash
261
261
  - Use 'restore' to get back accidentally deleted files
262
262
  - Use 'empty' periodically to free up disk space
263
- - All deletions by SafeShell are logged in .ai_safety.log
263
+ - All deletions by SafeShell are logged in ~/.clacky/safety_logs/
264
264
  HELP
265
265
 
266
266
  {
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Clacky
6
+ module UI
7
+ # ASCII art banner and startup screen with Matrix/hacker aesthetic
8
+ class Banner
9
+ LOGO = <<~'LOGO'
10
+ ██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗██╗ █████╗ ██████╗██╗ ██╗██╗ ██╗
11
+ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██╔══██╗██╔════╝██║ ██╔╝╚██╗ ██╔╝
12
+ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ██║ ███████║██║ █████╔╝ ╚████╔╝
13
+ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██╔══██║██║ ██╔═██╗ ╚██╔╝
14
+ ╚██████╔╝██║ ███████╗██║ ╚████║╚██████╗███████╗██║ ██║╚██████╗██║ ██╗ ██║
15
+ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝
16
+ LOGO
17
+
18
+ TAGLINE = "[>] AI Coding Assistant & Technical Co-founder"
19
+
20
+ TIPS = [
21
+ "[*] Ask questions, edit files, or run commands",
22
+ "[*] Be specific for the best results",
23
+ "[*] Create .clackyrules to customize interactions",
24
+ "[*] Type /help for more commands"
25
+ ]
26
+
27
+ def initialize
28
+ @pastel = Pastel.new
29
+ end
30
+
31
+ # Display full startup banner
32
+ def display_startup
33
+ puts
34
+ puts @pastel.bright_green(LOGO)
35
+ puts
36
+ puts @pastel.bright_cyan(TAGLINE)
37
+ puts
38
+ display_tips
39
+ puts
40
+ end
41
+
42
+ # Display welcome message for agent mode
43
+ def display_agent_welcome(working_dir:, mode:, max_iterations:, max_cost:)
44
+ puts
45
+ puts separator("=")
46
+ puts @pastel.bright_green("[+] AGENT MODE INITIALIZED")
47
+ puts separator("=")
48
+ puts
49
+ puts info_line("Working Directory", working_dir)
50
+ puts info_line("Permission Mode", mode)
51
+ puts info_line("Max Iterations", max_iterations)
52
+ puts info_line("Max Cost", "$#{max_cost}")
53
+ puts
54
+ puts @pastel.dim("[!] Type 'exit' or 'quit' to terminate session")
55
+ puts separator("-")
56
+ puts
57
+ end
58
+
59
+ # Display session continuation info
60
+ def display_session_continue(session_id:, created_at:, tasks:, cost:)
61
+ puts
62
+ puts separator("=")
63
+ puts @pastel.bright_yellow("[~] RESUMING SESSION")
64
+ puts separator("=")
65
+ puts
66
+ puts info_line("Session ID", session_id)
67
+ puts info_line("Started", created_at)
68
+ puts info_line("Tasks Completed", tasks)
69
+ puts info_line("Total Cost", "$#{cost}")
70
+ puts separator("-")
71
+ puts
72
+ end
73
+
74
+ # Display task completion summary
75
+ def display_task_complete(iterations:, cost:, total_tasks:, total_cost:, cache_stats: {})
76
+ puts
77
+ puts separator("-")
78
+ puts @pastel.bright_green("[✓] TASK COMPLETED")
79
+ puts info_line("Iterations", iterations)
80
+ puts info_line("Cost", "$#{cost}")
81
+ puts info_line("Session Total", "#{total_tasks} tasks, $#{total_cost}")
82
+
83
+ # Display cache statistics if available
84
+ if cache_stats[:total_requests] && cache_stats[:total_requests] > 0
85
+ puts
86
+ puts @pastel.cyan(" [Prompt Caching]")
87
+ puts info_line(" Cache Writes", "#{cache_stats[:cache_creation_input_tokens]} tokens")
88
+ puts info_line(" Cache Reads", "#{cache_stats[:cache_read_input_tokens]} tokens")
89
+
90
+ hit_rate = (cache_stats[:cache_hit_requests].to_f / cache_stats[:total_requests] * 100).round(1)
91
+ puts info_line(" Cache Hit Rate", "#{hit_rate}% (#{cache_stats[:cache_hit_requests]}/#{cache_stats[:total_requests]} requests)")
92
+ end
93
+
94
+ puts separator("-")
95
+ puts
96
+ end
97
+
98
+ # Display error message
99
+ def display_error(message, details: nil)
100
+ puts
101
+ puts separator("-")
102
+ puts @pastel.bright_red("[✗] ERROR")
103
+ puts @pastel.red(" #{message}")
104
+ if details
105
+ puts @pastel.dim(" #{details}")
106
+ end
107
+ puts separator("-")
108
+ puts
109
+ end
110
+
111
+ # Display session end
112
+ def display_goodbye(total_tasks:, total_cost:)
113
+ puts
114
+ puts separator("=")
115
+ puts @pastel.bright_cyan("[#] SESSION TERMINATED")
116
+ puts separator("=")
117
+ puts
118
+ puts info_line("Total Tasks", total_tasks)
119
+ puts info_line("Total Cost", "$#{total_cost}")
120
+ puts
121
+ puts @pastel.dim("[*] All session data has been saved")
122
+ puts
123
+ end
124
+
125
+ private
126
+
127
+ def display_tips
128
+ TIPS.each do |tip|
129
+ puts @pastel.dim(tip)
130
+ end
131
+ end
132
+
133
+ def info_line(label, value)
134
+ label_text = @pastel.cyan("[#{label}]")
135
+ value_text = @pastel.white(value)
136
+ " #{label_text} #{value_text}"
137
+ end
138
+
139
+ def separator(char = "-")
140
+ @pastel.dim(char * 80)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Clacky
6
+ module UI
7
+ # Matrix/hacker-style output formatter
8
+ class Formatter
9
+ # Hacker-style symbols (no emoji)
10
+ SYMBOLS = {
11
+ user: "[>>]",
12
+ assistant: "[<<]",
13
+ tool_call: "[=>]",
14
+ tool_result: "[<=]",
15
+ tool_denied: "[!!]",
16
+ tool_planned: "[??]",
17
+ tool_error: "[XX]",
18
+ thinking: "[..]",
19
+ success: "[OK]",
20
+ error: "[ER]",
21
+ warning: "[!!]",
22
+ info: "[--]",
23
+ task: "[##]",
24
+ progress: "[>>]"
25
+ }.freeze
26
+
27
+ def initialize
28
+ @pastel = Pastel.new
29
+ end
30
+
31
+ # Format user message
32
+ def user_message(content)
33
+ symbol = @pastel.bright_blue(SYMBOLS[:user])
34
+ text = @pastel.blue(content)
35
+ puts "\n#{symbol} #{text}"
36
+ end
37
+
38
+ # Format assistant message
39
+ def assistant_message(content)
40
+ return if content.nil? || content.empty?
41
+
42
+ symbol = @pastel.bright_green(SYMBOLS[:assistant])
43
+ text = @pastel.white(content)
44
+ puts "\n#{symbol} #{text}"
45
+ end
46
+
47
+ # Format tool call
48
+ def tool_call(formatted_call)
49
+ symbol = @pastel.bright_cyan(SYMBOLS[:tool_call])
50
+ text = @pastel.cyan(formatted_call)
51
+ puts "\n#{symbol} #{text}"
52
+ end
53
+
54
+ # Format tool result
55
+ def tool_result(summary)
56
+ symbol = @pastel.cyan(SYMBOLS[:tool_result])
57
+ text = @pastel.white(summary)
58
+ puts "#{symbol} #{text}"
59
+ end
60
+
61
+ # Format tool denied
62
+ def tool_denied(tool_name)
63
+ symbol = @pastel.bright_yellow(SYMBOLS[:tool_denied])
64
+ text = @pastel.yellow("Tool denied: #{tool_name}")
65
+ puts "\n#{symbol} #{text}"
66
+ end
67
+
68
+ # Format tool planned
69
+ def tool_planned(tool_name)
70
+ symbol = @pastel.bright_blue(SYMBOLS[:tool_planned])
71
+ text = @pastel.blue("Planned: #{tool_name}")
72
+ puts "\n#{symbol} #{text}"
73
+ end
74
+
75
+ # Format tool error
76
+ def tool_error(error_message)
77
+ symbol = @pastel.bright_red(SYMBOLS[:tool_error])
78
+ text = @pastel.red("Error: #{error_message}")
79
+ puts "\n#{symbol} #{text}"
80
+ end
81
+
82
+ # Format thinking indicator
83
+ def thinking
84
+ symbol = @pastel.dim(SYMBOLS[:thinking])
85
+ # Output symbol on the same line, progress indicator will follow
86
+ print "\n#{symbol} "
87
+ end
88
+
89
+ # Format success message
90
+ def success(message)
91
+ symbol = @pastel.bright_green(SYMBOLS[:success])
92
+ text = @pastel.green(message)
93
+ puts "#{symbol} #{text}"
94
+ end
95
+
96
+ # Format error message
97
+ def error(message)
98
+ symbol = @pastel.bright_red(SYMBOLS[:error])
99
+ text = @pastel.red(message)
100
+ puts "#{symbol} #{text}"
101
+ end
102
+
103
+ # Format warning message
104
+ def warning(message)
105
+ symbol = @pastel.bright_yellow(SYMBOLS[:warning])
106
+ text = @pastel.yellow(message)
107
+ puts "#{symbol} #{text}"
108
+ end
109
+
110
+ # Format info message
111
+ def info(message)
112
+ symbol = @pastel.bright_white(SYMBOLS[:info])
113
+ text = @pastel.white(message)
114
+ puts "#{symbol} #{text}"
115
+ end
116
+
117
+ # Format TODO status with progress bar
118
+ def todo_status(todos)
119
+ return if todos.empty?
120
+
121
+ completed = todos.count { |t| t[:status] == "completed" }
122
+ total = todos.size
123
+
124
+ # Build progress bar with hacker style
125
+ progress_bar = todos.map { |t|
126
+ t[:status] == "completed" ? @pastel.green("█") : @pastel.dim("░")
127
+ }.join
128
+
129
+ # Check if all completed
130
+ if completed == total
131
+ symbol = @pastel.bright_green(SYMBOLS[:success])
132
+ puts "\n#{symbol} Tasks [#{completed}/#{total}]: #{progress_bar} #{@pastel.bright_green('COMPLETE')}"
133
+ return
134
+ end
135
+
136
+ # Find current and next tasks
137
+ current_task = todos.find { |t| t[:status] == "pending" }
138
+ next_task_index = todos.index(current_task)
139
+ next_task = next_task_index && todos[next_task_index + 1]
140
+
141
+ symbol = @pastel.bright_yellow(SYMBOLS[:task])
142
+ puts "\n#{symbol} Tasks [#{completed}/#{total}]: #{progress_bar}"
143
+
144
+ if current_task
145
+ puts " #{@pastel.cyan('→')} Next: ##{current_task[:id]} - #{current_task[:task]}"
146
+ end
147
+
148
+ if next_task && next_task[:status] == "pending"
149
+ puts " #{@pastel.dim('⇢')} After: ##{next_task[:id]} - #{next_task[:task]}"
150
+ end
151
+ end
152
+
153
+ # Format iteration indicator
154
+ def iteration(number)
155
+ symbol = @pastel.dim(SYMBOLS[:progress])
156
+ text = @pastel.dim("Iteration #{number}")
157
+ puts "\n#{symbol} #{text}"
158
+ end
159
+
160
+ # Format separator
161
+ def separator(char = "─", width: 80)
162
+ puts @pastel.dim(char * width)
163
+ end
164
+
165
+ # Format section header
166
+ def section_header(title)
167
+ puts
168
+ separator("═")
169
+ puts @pastel.bright_white(title.center(80))
170
+ separator("═")
171
+ puts
172
+ end
173
+
174
+ # Format confirmation prompt for tool use
175
+ def tool_confirmation_prompt(formatted_call)
176
+ symbol = @pastel.bright_yellow("[??]")
177
+ puts "\n#{symbol} #{@pastel.yellow(formatted_call)}"
178
+ end
179
+
180
+ # Format conversation history message
181
+ def history_message(role, content, index, total)
182
+ case role
183
+ when "user"
184
+ symbol = @pastel.blue(SYMBOLS[:user])
185
+ text = @pastel.white(truncate(content, 150))
186
+ puts "#{symbol} You: #{text}"
187
+ when "assistant"
188
+ symbol = @pastel.green(SYMBOLS[:assistant])
189
+ text = @pastel.white(truncate(content, 200))
190
+ puts "#{symbol} Assistant: #{text}"
191
+ end
192
+ end
193
+
194
+ private
195
+
196
+ def truncate(content, max_length)
197
+ return "" if content.nil? || content.empty?
198
+
199
+ cleaned = content.strip.gsub(/\s+/, ' ')
200
+
201
+ if cleaned.length > max_length
202
+ cleaned[0...max_length] + "..."
203
+ else
204
+ cleaned
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "readline"
4
+ require "pastel"
5
+ require "tty-screen"
6
+
7
+ module Clacky
8
+ module UI
9
+ # Enhanced input prompt with box drawing and status info
10
+ class Prompt
11
+ def initialize
12
+ @pastel = Pastel.new
13
+ end
14
+
15
+ # Read user input with enhanced prompt box
16
+ # @param prefix [String] Prompt prefix (default: "You:")
17
+ # @param placeholder [String] Placeholder text (not shown when using Readline)
18
+ # @return [String, nil] User input or nil on EOF
19
+ def read_input(prefix: "You:", placeholder: nil)
20
+ width = [TTY::Screen.width - 5, 70].min
21
+
22
+ # Display complete box frame first
23
+ puts @pastel.dim("╭" + "─" * width + "╮")
24
+
25
+ # Empty input line with borders (width - 2 for left/right padding)
26
+ padding = " " * (width - 2)
27
+ puts @pastel.dim("│ #{padding} │")
28
+
29
+ # Bottom border
30
+ puts @pastel.dim("╰" + "─" * width + "╯")
31
+
32
+ # Move cursor back up to input line (2 lines up)
33
+ print "\e[2A" # Move up 2 lines
34
+ print "\r" # Move to beginning of line
35
+ print "\e[2C" # Move right 2 chars to after "│ "
36
+
37
+ # Read input with Readline
38
+ prompt_text = @pastel.bright_blue("#{prefix} ")
39
+ input = read_with_readline(prompt_text)
40
+
41
+ # After input, clear the input box completely
42
+ # Move cursor up 2 lines to the top of the box
43
+ print "\e[2A"
44
+ print "\r"
45
+
46
+ # Clear all 3 lines of the box
47
+ 3.times do
48
+ print "\e[2K" # Clear entire line
49
+ print "\e[1B" # Move down 1 line
50
+ print "\r" # Move to beginning of line
51
+ end
52
+
53
+ # Move cursor back up to where the box started
54
+ print "\e[3A"
55
+ print "\r"
56
+
57
+ input
58
+ end
59
+
60
+ private
61
+
62
+ def read_with_readline(prompt)
63
+ Readline.readline(prompt, true)
64
+ rescue Interrupt
65
+ puts
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+ require "tty-screen"
5
+
6
+ module Clacky
7
+ module UI
8
+ # Status bar showing session information
9
+ class StatusBar
10
+ def initialize
11
+ @pastel = Pastel.new
12
+ end
13
+
14
+ # Display session status bar
15
+ # @param working_dir [String] Current working directory
16
+ # @param mode [String] Permission mode
17
+ # @param model [String] AI model name
18
+ # @param tasks [Integer] Number of completed tasks (optional)
19
+ # @param cost [Float] Total cost (optional)
20
+ def display(working_dir:, mode:, model:, tasks: nil, cost: nil)
21
+ parts = []
22
+
23
+ # Working directory (shortened if too long)
24
+ dir_display = shorten_path(working_dir)
25
+ parts << @pastel.bright_cyan(dir_display)
26
+
27
+ # Permission mode
28
+ mode_color = mode_color_for(mode)
29
+ parts << @pastel.public_send(mode_color, mode)
30
+
31
+ # Model
32
+ parts << @pastel.bright_white(model)
33
+
34
+ # Optional: tasks and cost
35
+ if tasks
36
+ parts << @pastel.yellow("#{tasks} tasks")
37
+ end
38
+
39
+ if cost
40
+ parts << @pastel.yellow("$#{cost.round(4)}")
41
+ end
42
+
43
+ # Join with separator
44
+ separator = @pastel.dim(" │ ")
45
+ status_line = " " + parts.join(separator)
46
+
47
+ puts status_line
48
+ puts @pastel.dim("─" * [TTY::Screen.width, 80].min)
49
+ end
50
+
51
+ # Display minimal status for non-interactive mode
52
+ def display_minimal(working_dir:, mode:)
53
+ dir_display = shorten_path(working_dir)
54
+ puts " #{@pastel.bright_cyan(dir_display)} #{@pastel.dim('│')} #{@pastel.yellow(mode)}"
55
+ puts @pastel.dim("─" * [TTY::Screen.width, 80].min)
56
+ end
57
+
58
+ private
59
+
60
+ def shorten_path(path)
61
+ return path if path.length <= 40
62
+
63
+ # Replace home directory with ~
64
+ home = ENV['HOME']
65
+ if home && path.start_with?(home)
66
+ path = path.sub(home, '~')
67
+ end
68
+
69
+ # If still too long, show last parts
70
+ if path.length > 40
71
+ parts = path.split('/')
72
+ if parts.length > 3
73
+ ".../" + parts[-3..-1].join('/')
74
+ else
75
+ path[0..40] + "..."
76
+ end
77
+ else
78
+ path
79
+ end
80
+ end
81
+
82
+ def mode_color_for(mode)
83
+ case mode.to_s
84
+ when /auto_approve/
85
+ :bright_red
86
+ when /confirm_safes/
87
+ :bright_yellow
88
+ when /confirm_edits/
89
+ :bright_green
90
+ when /plan_only/
91
+ :bright_blue
92
+ else
93
+ :white
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
5
5
  end
data/lib/clacky.rb CHANGED
@@ -30,6 +30,12 @@ require_relative "clacky/tools/safe_shell"
30
30
  require_relative "clacky/tools/trash_manager"
31
31
  require_relative "clacky/agent"
32
32
 
33
+ # UI components
34
+ require_relative "clacky/ui/banner"
35
+ require_relative "clacky/ui/prompt"
36
+ require_relative "clacky/ui/statusbar"
37
+ require_relative "clacky/ui/formatter"
38
+
33
39
  require_relative "clacky/cli"
34
40
 
35
41
  module Clacky
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -79,6 +79,34 @@ dependencies:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: '3.4'
82
+ - !ruby/object:Gem::Dependency
83
+ name: pastel
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.8'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.8'
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'
82
110
  description: OpenClacky is a Ruby CLI tool for interacting with AI models via OpenAI-compatible
83
111
  APIs. It provides chat functionality and autonomous AI agent capabilities with tool
84
112
  use.
@@ -128,6 +156,10 @@ files:
128
156
  - lib/clacky/tools/web_search.rb
129
157
  - lib/clacky/tools/write.rb
130
158
  - lib/clacky/trash_directory.rb
159
+ - lib/clacky/ui/banner.rb
160
+ - lib/clacky/ui/formatter.rb
161
+ - lib/clacky/ui/prompt.rb
162
+ - lib/clacky/ui/statusbar.rb
131
163
  - lib/clacky/utils/arguments_parser.rb
132
164
  - lib/clacky/utils/limit_stack.rb
133
165
  - lib/clacky/utils/path_helper.rb