openclacky 0.6.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +39 -88
- data/homebrew/README.md +96 -0
- data/homebrew/openclacky.rb +24 -0
- data/lib/clacky/agent.rb +139 -67
- data/lib/clacky/cli.rb +105 -6
- data/lib/clacky/tools/file_reader.rb +135 -2
- data/lib/clacky/tools/glob.rb +2 -2
- data/lib/clacky/tools/grep.rb +2 -2
- data/lib/clacky/tools/run_project.rb +5 -5
- data/lib/clacky/tools/safe_shell.rb +140 -17
- data/lib/clacky/tools/shell.rb +69 -2
- data/lib/clacky/tools/todo_manager.rb +50 -3
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/web_fetch.rb +2 -2
- data/lib/clacky/tools/web_search.rb +2 -2
- data/lib/clacky/ui2/components/common_component.rb +14 -5
- data/lib/clacky/ui2/components/input_area.rb +300 -89
- data/lib/clacky/ui2/components/message_component.rb +7 -3
- data/lib/clacky/ui2/components/todo_area.rb +38 -45
- data/lib/clacky/ui2/components/welcome_banner.rb +10 -0
- data/lib/clacky/ui2/layout_manager.rb +180 -50
- data/lib/clacky/ui2/markdown_renderer.rb +80 -0
- data/lib/clacky/ui2/screen_buffer.rb +26 -7
- data/lib/clacky/ui2/themes/base_theme.rb +32 -46
- data/lib/clacky/ui2/themes/hacker_theme.rb +4 -2
- data/lib/clacky/ui2/themes/minimal_theme.rb +4 -2
- data/lib/clacky/ui2/ui_controller.rb +150 -32
- data/lib/clacky/ui2/view_renderer.rb +21 -4
- data/lib/clacky/ui2.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/utils/file_processor.rb +201 -0
- data/lib/clacky/version.rb +1 -1
- data/scripts/install.sh +249 -0
- data/scripts/uninstall.sh +146 -0
- metadata +21 -2
- data/lib/clacky/ui2/components/output_area.rb +0 -112
data/lib/clacky/agent.rb
CHANGED
|
@@ -4,8 +4,8 @@ require "securerandom"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "tty-prompt"
|
|
6
6
|
require "set"
|
|
7
|
-
require "base64"
|
|
8
7
|
require_relative "utils/arguments_parser"
|
|
8
|
+
require_relative "utils/file_processor"
|
|
9
9
|
|
|
10
10
|
module Clacky
|
|
11
11
|
class Agent
|
|
@@ -129,6 +129,33 @@ module Clacky
|
|
|
129
129
|
end
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# Get recent user messages from conversation history
|
|
133
|
+
# @param limit [Integer] Number of recent user messages to retrieve (default: 5)
|
|
134
|
+
# @return [Array<String>] Array of recent user message contents
|
|
135
|
+
def get_recent_user_messages(limit: 5)
|
|
136
|
+
# Filter messages to only include real user messages (exclude system-injected ones)
|
|
137
|
+
user_messages = @messages.select do |m|
|
|
138
|
+
m[:role] == "user" && !m[:system_injected]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Extract text content from the last N user messages
|
|
142
|
+
user_messages.last(limit).map do |msg|
|
|
143
|
+
extract_text_from_content(msg[:content])
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private def extract_text_from_content(content)
|
|
148
|
+
if content.is_a?(String)
|
|
149
|
+
content
|
|
150
|
+
elsif content.is_a?(Array)
|
|
151
|
+
# Extract text from content array (may contain text and images)
|
|
152
|
+
text_parts = content.select { |c| c.is_a?(Hash) && c[:type] == "text" }
|
|
153
|
+
text_parts.map { |c| c[:text] }.join("\n")
|
|
154
|
+
else
|
|
155
|
+
content.to_s
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
132
159
|
def add_hook(event, &block)
|
|
133
160
|
@hooks.add(event, &block)
|
|
134
161
|
end
|
|
@@ -136,7 +163,18 @@ module Clacky
|
|
|
136
163
|
def run(user_input, images: [])
|
|
137
164
|
@start_time = Time.now
|
|
138
165
|
@task_cost_source = :estimated # Reset for new task
|
|
139
|
-
@previous_total_tokens
|
|
166
|
+
# Note: Do NOT reset @previous_total_tokens here - it should maintain the value from the last iteration
|
|
167
|
+
# across tasks to correctly calculate delta tokens in each iteration
|
|
168
|
+
@task_start_iterations = @iterations # Track starting iterations for this task
|
|
169
|
+
@task_start_cost = @total_cost # Track starting cost for this task
|
|
170
|
+
|
|
171
|
+
# Track cache stats for current task
|
|
172
|
+
@task_cache_stats = {
|
|
173
|
+
cache_creation_input_tokens: 0,
|
|
174
|
+
cache_read_input_tokens: 0,
|
|
175
|
+
total_requests: 0,
|
|
176
|
+
cache_hit_requests: 0
|
|
177
|
+
}
|
|
140
178
|
|
|
141
179
|
# Add system prompt as the first message if this is the first run
|
|
142
180
|
if @messages.empty?
|
|
@@ -193,16 +231,17 @@ module Clacky
|
|
|
193
231
|
if action_result[:denied]
|
|
194
232
|
# If user provided feedback, treat it as a user question/instruction
|
|
195
233
|
if action_result[:feedback] && !action_result[:feedback].empty?
|
|
196
|
-
# Add user feedback as a new user message
|
|
234
|
+
# Add user feedback as a new user message with system_injected marker
|
|
197
235
|
@messages << {
|
|
198
236
|
role: "user",
|
|
199
|
-
content: "STOP. The user has a question/feedback for you: #{action_result[:feedback]}\n\nPlease respond to the user's question/feedback before continuing with any actions."
|
|
237
|
+
content: "STOP. The user has a question/feedback for you: #{action_result[:feedback]}\n\nPlease respond to the user's question/feedback before continuing with any actions.",
|
|
238
|
+
system_injected: true # Mark as system-injected message for filtering
|
|
200
239
|
}
|
|
201
240
|
# Continue loop to let agent respond to feedback
|
|
202
241
|
next
|
|
203
242
|
else
|
|
204
243
|
# User just said "no" without feedback - stop and wait
|
|
205
|
-
@ui&.show_assistant_message("Tool execution was denied. Please
|
|
244
|
+
@ui&.show_assistant_message("Tool execution was denied. Please give more instructions...")
|
|
206
245
|
break
|
|
207
246
|
end
|
|
208
247
|
end
|
|
@@ -221,8 +260,8 @@ module Clacky
|
|
|
221
260
|
# Let CLI handle the interrupt message
|
|
222
261
|
raise
|
|
223
262
|
rescue StandardError => e
|
|
263
|
+
# Build error result for session data, but let CLI handle error display
|
|
224
264
|
result = build_result(:error, error: e.message)
|
|
225
|
-
@ui&.show_error("Error: #{e.message}")
|
|
226
265
|
raise
|
|
227
266
|
end
|
|
228
267
|
end
|
|
@@ -405,8 +444,8 @@ module Clacky
|
|
|
405
444
|
end
|
|
406
445
|
end
|
|
407
446
|
|
|
408
|
-
#
|
|
409
|
-
@ui&.
|
|
447
|
+
# Clear progress indicator (change to gray and show final time)
|
|
448
|
+
@ui&.clear_progress
|
|
410
449
|
|
|
411
450
|
track_cost(response[:usage], raw_api_usage: response[:raw_api_usage])
|
|
412
451
|
|
|
@@ -494,7 +533,13 @@ module Clacky
|
|
|
494
533
|
|
|
495
534
|
confirmation = confirm_tool_use?(call)
|
|
496
535
|
unless confirmation[:approved]
|
|
497
|
-
|
|
536
|
+
# Show denial warning with user feedback if provided
|
|
537
|
+
denial_message = "Tool #{call[:name]} denied"
|
|
538
|
+
if confirmation[:feedback] && !confirmation[:feedback].empty?
|
|
539
|
+
denial_message += ": #{confirmation[:feedback]}"
|
|
540
|
+
end
|
|
541
|
+
@ui&.show_warning(denial_message)
|
|
542
|
+
|
|
498
543
|
denied = true
|
|
499
544
|
user_feedback = confirmation[:feedback]
|
|
500
545
|
feedback = user_feedback if user_feedback
|
|
@@ -526,8 +571,17 @@ module Clacky
|
|
|
526
571
|
args[:todos_storage] = @todos
|
|
527
572
|
end
|
|
528
573
|
|
|
574
|
+
# Show progress for potentially slow tools (no prefix newline)
|
|
575
|
+
if potentially_slow_tool?(call[:name], args)
|
|
576
|
+
progress_message = build_tool_progress_message(call[:name], args)
|
|
577
|
+
@ui&.show_progress(progress_message, prefix_newline: false)
|
|
578
|
+
end
|
|
579
|
+
|
|
529
580
|
result = tool.execute(**args)
|
|
530
581
|
|
|
582
|
+
# Clear progress if shown
|
|
583
|
+
@ui&.clear_progress if potentially_slow_tool?(call[:name], args)
|
|
584
|
+
|
|
531
585
|
# Hook: after_tool_use
|
|
532
586
|
@hooks.trigger(:after_tool_use, call, result)
|
|
533
587
|
|
|
@@ -606,6 +660,49 @@ module Clacky
|
|
|
606
660
|
false
|
|
607
661
|
end
|
|
608
662
|
|
|
663
|
+
# Check if a tool is potentially slow and should show progress
|
|
664
|
+
private def potentially_slow_tool?(tool_name, args)
|
|
665
|
+
case tool_name.to_s.downcase
|
|
666
|
+
when 'shell', 'safe_shell'
|
|
667
|
+
# Check if the command is a slow command
|
|
668
|
+
command = args[:command] || args['command']
|
|
669
|
+
return false unless command
|
|
670
|
+
|
|
671
|
+
# List of slow command patterns
|
|
672
|
+
slow_patterns = [
|
|
673
|
+
/bundle\s+(install|exec\s+rspec|exec\s+rake)/,
|
|
674
|
+
/npm\s+(install|run\s+test|run\s+build)/,
|
|
675
|
+
/yarn\s+(install|test|build)/,
|
|
676
|
+
/pnpm\s+install/,
|
|
677
|
+
/cargo\s+(build|test)/,
|
|
678
|
+
/go\s+(build|test)/,
|
|
679
|
+
/make\s+(test|build)/,
|
|
680
|
+
/pytest/,
|
|
681
|
+
/jest/
|
|
682
|
+
]
|
|
683
|
+
|
|
684
|
+
slow_patterns.any? { |pattern| command.match?(pattern) }
|
|
685
|
+
when 'web_fetch', 'web_search'
|
|
686
|
+
true # Network operations can be slow
|
|
687
|
+
else
|
|
688
|
+
false # Most file operations are fast
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Build progress message for tool execution
|
|
693
|
+
private def build_tool_progress_message(tool_name, args)
|
|
694
|
+
case tool_name.to_s.downcase
|
|
695
|
+
when 'shell', 'safe_shell'
|
|
696
|
+
"Running command"
|
|
697
|
+
when 'web_fetch'
|
|
698
|
+
"Fetching web page"
|
|
699
|
+
when 'web_search'
|
|
700
|
+
"Searching web"
|
|
701
|
+
else
|
|
702
|
+
"Executing #{tool_name}"
|
|
703
|
+
end
|
|
704
|
+
end
|
|
705
|
+
|
|
609
706
|
def track_cost(usage, raw_api_usage: nil)
|
|
610
707
|
# Priority 1: Use API-provided cost if available (OpenRouter, LiteLLM, etc.)
|
|
611
708
|
iteration_cost = nil
|
|
@@ -637,7 +734,7 @@ module Clacky
|
|
|
637
734
|
# Display token usage statistics for this iteration
|
|
638
735
|
display_iteration_tokens(usage, iteration_cost)
|
|
639
736
|
|
|
640
|
-
# Track cache usage statistics
|
|
737
|
+
# Track cache usage statistics (global)
|
|
641
738
|
@cache_stats[:total_requests] += 1
|
|
642
739
|
|
|
643
740
|
if usage[:cache_creation_input_tokens]
|
|
@@ -655,6 +752,20 @@ module Clacky
|
|
|
655
752
|
@cache_stats[:raw_api_usage_samples] << raw_api_usage
|
|
656
753
|
@cache_stats[:raw_api_usage_samples] = @cache_stats[:raw_api_usage_samples].last(3)
|
|
657
754
|
end
|
|
755
|
+
|
|
756
|
+
# Track cache usage for current task
|
|
757
|
+
if @task_cache_stats
|
|
758
|
+
@task_cache_stats[:total_requests] += 1
|
|
759
|
+
|
|
760
|
+
if usage[:cache_creation_input_tokens]
|
|
761
|
+
@task_cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
if usage[:cache_read_input_tokens]
|
|
765
|
+
@task_cache_stats[:cache_read_input_tokens] += usage[:cache_read_input_tokens]
|
|
766
|
+
@task_cache_stats[:cache_hit_requests] += 1
|
|
767
|
+
end
|
|
768
|
+
end
|
|
658
769
|
end
|
|
659
770
|
|
|
660
771
|
# Display token usage for current iteration
|
|
@@ -843,7 +954,8 @@ module Clacky
|
|
|
843
954
|
|
|
844
955
|
{
|
|
845
956
|
role: "user",
|
|
846
|
-
content: "[SYSTEM] " + summary_text
|
|
957
|
+
content: "[SYSTEM] " + summary_text,
|
|
958
|
+
system_injected: true
|
|
847
959
|
}
|
|
848
960
|
end
|
|
849
961
|
|
|
@@ -867,9 +979,14 @@ module Clacky
|
|
|
867
979
|
when true
|
|
868
980
|
{ approved: true, feedback: nil }
|
|
869
981
|
when false, nil
|
|
982
|
+
# User denied - add visual marker based on tool type
|
|
983
|
+
tool_name_capitalized = call[:name].capitalize
|
|
984
|
+
@ui&.show_info(" ↳ #{tool_name_capitalized} cancelled", prefix_newline: false)
|
|
870
985
|
{ approved: false, feedback: nil }
|
|
871
986
|
else
|
|
872
|
-
# String feedback
|
|
987
|
+
# String feedback - also add visual marker
|
|
988
|
+
tool_name_capitalized = call[:name].capitalize
|
|
989
|
+
@ui&.show_info(" ↳ #{tool_name_capitalized} cancelled", prefix_newline: false)
|
|
873
990
|
{ approved: false, feedback: result.to_s }
|
|
874
991
|
end
|
|
875
992
|
else
|
|
@@ -1079,14 +1196,20 @@ module Clacky
|
|
|
1079
1196
|
end
|
|
1080
1197
|
|
|
1081
1198
|
def build_result(status, error: nil)
|
|
1199
|
+
# Calculate iterations for current task only
|
|
1200
|
+
task_iterations = @iterations - (@task_start_iterations || 0)
|
|
1201
|
+
|
|
1202
|
+
# Calculate cost for current task only
|
|
1203
|
+
task_cost = @total_cost - (@task_start_cost || 0)
|
|
1204
|
+
|
|
1082
1205
|
{
|
|
1083
1206
|
status: status,
|
|
1084
1207
|
session_id: @session_id,
|
|
1085
|
-
iterations:
|
|
1208
|
+
iterations: task_iterations, # Show only current task iterations
|
|
1086
1209
|
duration_seconds: Time.now - @start_time,
|
|
1087
|
-
total_cost_usd:
|
|
1210
|
+
total_cost_usd: task_cost.round(4), # Show only current task cost
|
|
1088
1211
|
cost_source: @task_cost_source, # Add cost source for this task
|
|
1089
|
-
cache_stats: @cache_stats,
|
|
1212
|
+
cache_stats: @task_cache_stats || @cache_stats, # Use task cache stats if available
|
|
1090
1213
|
messages: @messages,
|
|
1091
1214
|
error: error
|
|
1092
1215
|
}
|
|
@@ -1132,64 +1255,13 @@ module Clacky
|
|
|
1132
1255
|
content << { type: "text", text: text } unless text.nil? || text.empty?
|
|
1133
1256
|
|
|
1134
1257
|
images.each do |image_path|
|
|
1135
|
-
image_url = image_path_to_data_url(image_path)
|
|
1258
|
+
image_url = Utils::FileProcessor.image_path_to_data_url(image_path)
|
|
1136
1259
|
content << { type: "image_url", image_url: { url: image_url } }
|
|
1137
1260
|
end
|
|
1138
1261
|
|
|
1139
1262
|
content
|
|
1140
1263
|
end
|
|
1141
1264
|
|
|
1142
|
-
# Convert image file path to base64 data URL
|
|
1143
|
-
# @param path [String] File path to image
|
|
1144
|
-
# @return [String] base64 data URL (e.g., "data:image/png;base64,...")
|
|
1145
|
-
def image_path_to_data_url(path)
|
|
1146
|
-
unless File.exist?(path)
|
|
1147
|
-
raise ArgumentError, "Image file not found: #{path}"
|
|
1148
|
-
end
|
|
1149
|
-
|
|
1150
|
-
# Read file as binary
|
|
1151
|
-
image_data = File.binread(path)
|
|
1152
|
-
|
|
1153
|
-
# Detect MIME type from file extension or content
|
|
1154
|
-
mime_type = detect_image_mime_type(path, image_data)
|
|
1155
|
-
|
|
1156
|
-
# Encode to base64
|
|
1157
|
-
base64_data = Base64.strict_encode64(image_data)
|
|
1158
|
-
|
|
1159
|
-
"data:#{mime_type};base64,#{base64_data}"
|
|
1160
|
-
end
|
|
1161
1265
|
|
|
1162
|
-
# Detect image MIME type
|
|
1163
|
-
# @param path [String] File path
|
|
1164
|
-
# @param data [String] Binary image data
|
|
1165
|
-
# @return [String] MIME type (e.g., "image/png")
|
|
1166
|
-
def detect_image_mime_type(path, data)
|
|
1167
|
-
# Try to detect from file extension first
|
|
1168
|
-
ext = File.extname(path).downcase
|
|
1169
|
-
case ext
|
|
1170
|
-
when ".png"
|
|
1171
|
-
"image/png"
|
|
1172
|
-
when ".jpg", ".jpeg"
|
|
1173
|
-
"image/jpeg"
|
|
1174
|
-
when ".gif"
|
|
1175
|
-
"image/gif"
|
|
1176
|
-
when ".webp"
|
|
1177
|
-
"image/webp"
|
|
1178
|
-
else
|
|
1179
|
-
# Try to detect from file signature (magic bytes)
|
|
1180
|
-
if data.start_with?("\x89PNG".b)
|
|
1181
|
-
"image/png"
|
|
1182
|
-
elsif data.start_with?("\xFF\xD8\xFF".b)
|
|
1183
|
-
"image/jpeg"
|
|
1184
|
-
elsif data.start_with?("GIF87a".b) || data.start_with?("GIF89a".b)
|
|
1185
|
-
"image/gif"
|
|
1186
|
-
elsif data.start_with?("RIFF".b) && data[8..11] == "WEBP".b
|
|
1187
|
-
"image/webp"
|
|
1188
|
-
else
|
|
1189
|
-
# Default to png if unknown
|
|
1190
|
-
"image/png"
|
|
1191
|
-
end
|
|
1192
|
-
end
|
|
1193
|
-
end
|
|
1194
1266
|
end
|
|
1195
1267
|
end
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -32,6 +32,10 @@ module Clacky
|
|
|
32
32
|
confirm_edits - Auto-approve read-only tools, confirm edits
|
|
33
33
|
plan_only - Generate plan without executing
|
|
34
34
|
|
|
35
|
+
UI themes:
|
|
36
|
+
hacker - Matrix/hacker-style with bracket symbols (default)
|
|
37
|
+
minimal - Clean, simple symbols
|
|
38
|
+
|
|
35
39
|
Session management:
|
|
36
40
|
-c, --continue - Continue the most recent session for this directory
|
|
37
41
|
-l, --list - List recent sessions
|
|
@@ -42,6 +46,8 @@ module Clacky
|
|
|
42
46
|
LONGDESC
|
|
43
47
|
option :mode, type: :string, default: "confirm_safes",
|
|
44
48
|
desc: "Permission mode: auto_approve, confirm_safes, confirm_edits, plan_only"
|
|
49
|
+
option :theme, type: :string, default: "hacker",
|
|
50
|
+
desc: "UI theme: hacker, minimal (default: hacker)"
|
|
45
51
|
option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Show detailed output"
|
|
46
52
|
option :path, type: :string, desc: "Project directory path (defaults to current directory)"
|
|
47
53
|
option :continue, type: :boolean, aliases: "-c", desc: "Continue most recent session"
|
|
@@ -82,11 +88,14 @@ module Clacky
|
|
|
82
88
|
# Handle session loading/continuation
|
|
83
89
|
session_manager = Clacky::SessionManager.new
|
|
84
90
|
agent = nil
|
|
91
|
+
is_session_load = false
|
|
85
92
|
|
|
86
93
|
if options[:continue]
|
|
87
94
|
agent = load_latest_session(client, agent_config, session_manager, working_dir)
|
|
95
|
+
is_session_load = !agent.nil?
|
|
88
96
|
elsif options[:attach]
|
|
89
97
|
agent = load_session_by_number(client, agent_config, session_manager, working_dir, options[:attach])
|
|
98
|
+
is_session_load = !agent.nil?
|
|
90
99
|
end
|
|
91
100
|
|
|
92
101
|
# Create new agent if no session loaded
|
|
@@ -98,7 +107,7 @@ module Clacky
|
|
|
98
107
|
Dir.chdir(working_dir) if should_chdir
|
|
99
108
|
|
|
100
109
|
begin
|
|
101
|
-
run_agent_with_ui2(agent, working_dir, agent_config, message, session_manager, client)
|
|
110
|
+
run_agent_with_ui2(agent, working_dir, agent_config, message, session_manager, client, is_session_load: is_session_load)
|
|
102
111
|
rescue StandardError => e
|
|
103
112
|
# Save session on error
|
|
104
113
|
if session_manager
|
|
@@ -124,6 +133,80 @@ module Clacky
|
|
|
124
133
|
end
|
|
125
134
|
end
|
|
126
135
|
|
|
136
|
+
desc "new PROJECT_NAME", "Create a new Rails project from the official template"
|
|
137
|
+
long_desc <<-LONGDESC
|
|
138
|
+
Create a new Rails project from the official template.
|
|
139
|
+
|
|
140
|
+
This command will:
|
|
141
|
+
1. Clone the template from git@github.com:clacky-ai/rails-template-7x-starter.git
|
|
142
|
+
2. Change into the project directory
|
|
143
|
+
3. Run bin/setup to install dependencies and configure the project
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
$ clacky new my_rails_app
|
|
147
|
+
LONGDESC
|
|
148
|
+
def new(project_name = nil)
|
|
149
|
+
unless project_name
|
|
150
|
+
say "Error: Project name is required.", :red
|
|
151
|
+
say "Usage: clacky new <project_name>", :yellow
|
|
152
|
+
exit 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Validate project name
|
|
156
|
+
unless project_name.match?(/^[a-zA-Z][a-zA-Z0-9_-]*$/)
|
|
157
|
+
say "Error: Invalid project name. Use only letters, numbers, underscores, and hyphens.", :red
|
|
158
|
+
exit 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
template_repo = "git@github.com:clacky-ai/rails-template-7x-starter.git"
|
|
162
|
+
current_dir = Dir.pwd
|
|
163
|
+
target_dir = File.join(current_dir, project_name)
|
|
164
|
+
|
|
165
|
+
# Check if target directory already exists
|
|
166
|
+
if Dir.exist?(target_dir)
|
|
167
|
+
say "Error: Directory '#{project_name}' already exists.", :red
|
|
168
|
+
exit 1
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
say "Creating new Rails project: #{project_name}", :green
|
|
172
|
+
|
|
173
|
+
# Clone the template repository
|
|
174
|
+
say "\n📦 Cloning template repository...", :cyan
|
|
175
|
+
clone_command = "git clone #{template_repo} #{project_name}"
|
|
176
|
+
|
|
177
|
+
clone_result = system(clone_command)
|
|
178
|
+
|
|
179
|
+
unless clone_result
|
|
180
|
+
say "\n❌ Failed to clone repository. Please check your git configuration and network connection.", :red
|
|
181
|
+
exit 1
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
say "✓ Repository cloned successfully", :green
|
|
185
|
+
|
|
186
|
+
# Run bin/setup
|
|
187
|
+
say "\n⚙️ Running bin/setup...", :cyan
|
|
188
|
+
|
|
189
|
+
Dir.chdir(target_dir)
|
|
190
|
+
|
|
191
|
+
setup_command = "./bin/setup"
|
|
192
|
+
|
|
193
|
+
setup_result = system(setup_command)
|
|
194
|
+
|
|
195
|
+
Dir.chdir(current_dir)
|
|
196
|
+
|
|
197
|
+
unless setup_result
|
|
198
|
+
say "\n❌ Failed to run bin/setup. Please check the setup script for errors.", :red
|
|
199
|
+
say "You can try running it manually:", :yellow
|
|
200
|
+
say " cd #{project_name} && ./bin/setup", :cyan
|
|
201
|
+
exit 1
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
say "\n✅ Project '#{project_name}' created successfully!", :green
|
|
205
|
+
say "\nNext steps:", :green
|
|
206
|
+
say " cd #{project_name}", :cyan
|
|
207
|
+
say " clacky agent", :cyan
|
|
208
|
+
end
|
|
209
|
+
|
|
127
210
|
desc "price", "Show pricing information for AI models"
|
|
128
211
|
def price
|
|
129
212
|
say "\n💰 Model Pricing Information\n\n", :green
|
|
@@ -231,7 +314,7 @@ module Clacky
|
|
|
231
314
|
return nil
|
|
232
315
|
end
|
|
233
316
|
|
|
234
|
-
|
|
317
|
+
# Don't print message here - will be shown by UI after banner
|
|
235
318
|
Clacky::Agent.from_session(client, agent_config, session_data)
|
|
236
319
|
end
|
|
237
320
|
|
|
@@ -276,7 +359,7 @@ module Clacky
|
|
|
276
359
|
end
|
|
277
360
|
end
|
|
278
361
|
|
|
279
|
-
|
|
362
|
+
# Don't print message here - will be shown by UI after banner
|
|
280
363
|
Clacky::Agent.from_session(client, agent_config, session_data)
|
|
281
364
|
end
|
|
282
365
|
|
|
@@ -296,12 +379,21 @@ module Clacky
|
|
|
296
379
|
end
|
|
297
380
|
|
|
298
381
|
# Run agent with UI2 split-screen interface
|
|
299
|
-
def run_agent_with_ui2(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil)
|
|
382
|
+
def run_agent_with_ui2(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil, is_session_load: false)
|
|
383
|
+
# Validate theme
|
|
384
|
+
theme_name = options[:theme] || "hacker"
|
|
385
|
+
available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
|
|
386
|
+
unless available_themes.include?(theme_name)
|
|
387
|
+
say "Error: Unknown theme '#{theme_name}'. Available themes: #{available_themes.join(', ')}", :red
|
|
388
|
+
exit 1
|
|
389
|
+
end
|
|
390
|
+
|
|
300
391
|
# Create UI2 controller with configuration
|
|
301
392
|
ui_controller = UI2::UIController.new(
|
|
302
393
|
working_dir: working_dir,
|
|
303
394
|
mode: agent_config.permission_mode.to_s,
|
|
304
|
-
model: agent_config.model
|
|
395
|
+
model: agent_config.model,
|
|
396
|
+
theme: theme_name
|
|
305
397
|
)
|
|
306
398
|
|
|
307
399
|
# Inject UI into agent
|
|
@@ -359,6 +451,8 @@ module Clacky
|
|
|
359
451
|
ui_controller.show_info("Session cleared. Starting fresh.")
|
|
360
452
|
# Update session bar with reset values
|
|
361
453
|
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
454
|
+
# Clear todo area display
|
|
455
|
+
ui_controller.update_todos([])
|
|
362
456
|
next
|
|
363
457
|
when "/exit", "/quit"
|
|
364
458
|
ui_controller.stop
|
|
@@ -400,7 +494,12 @@ module Clacky
|
|
|
400
494
|
end
|
|
401
495
|
|
|
402
496
|
# Initialize UI screen first
|
|
403
|
-
|
|
497
|
+
if is_session_load
|
|
498
|
+
recent_user_messages = agent.get_recent_user_messages(limit: 5)
|
|
499
|
+
ui_controller.initialize_and_show_banner(recent_user_messages: recent_user_messages)
|
|
500
|
+
else
|
|
501
|
+
ui_controller.initialize_and_show_banner
|
|
502
|
+
end
|
|
404
503
|
|
|
405
504
|
# If there's an initial message, process it
|
|
406
505
|
if initial_message && !initial_message.strip.empty?
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
|
+
require_relative "../utils/file_processor"
|
|
4
5
|
|
|
5
6
|
module Clacky
|
|
6
7
|
module Tools
|
|
@@ -23,6 +24,8 @@ module Clacky
|
|
|
23
24
|
},
|
|
24
25
|
required: ["path"]
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
|
|
26
29
|
|
|
27
30
|
def execute(path:, max_lines: 1000)
|
|
28
31
|
# Expand ~ to home directory
|
|
@@ -50,6 +53,12 @@ module Clacky
|
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
begin
|
|
56
|
+
# Check if file is binary
|
|
57
|
+
if binary_file?(expanded_path)
|
|
58
|
+
return handle_binary_file(expanded_path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Read text file
|
|
53
62
|
lines = File.readlines(expanded_path).first(max_lines)
|
|
54
63
|
content = lines.join
|
|
55
64
|
truncated = File.readlines(expanded_path).size > max_lines
|
|
@@ -86,16 +95,140 @@ module Clacky
|
|
|
86
95
|
return "Listed #{entries} entries (#{dirs} directories, #{files} files)"
|
|
87
96
|
end
|
|
88
97
|
|
|
89
|
-
# Handle file
|
|
98
|
+
# Handle binary file
|
|
99
|
+
if result[:binary] || result['binary']
|
|
100
|
+
format_type = result[:format] || result['format'] || 'unknown'
|
|
101
|
+
size = result[:size_bytes] || result['size_bytes'] || 0
|
|
102
|
+
|
|
103
|
+
# Check if it has base64 data (LLM-compatible format)
|
|
104
|
+
if result[:base64_data] || result['base64_data']
|
|
105
|
+
size_warning = size > 5_000_000 ? " (WARNING: large file)" : ""
|
|
106
|
+
return "Binary file (#{format_type}, #{format_file_size(size)}) - sent to LLM#{size_warning}"
|
|
107
|
+
else
|
|
108
|
+
return "Binary file (#{format_type}, #{format_file_size(size)}) - cannot be read as text"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Handle text file reading
|
|
90
113
|
lines = result[:lines_read] || result['lines_read'] || 0
|
|
91
114
|
truncated = result[:truncated] || result['truncated']
|
|
92
115
|
"Read #{lines} lines#{truncated ? ' (truncated)' : ''}"
|
|
93
116
|
end
|
|
117
|
+
|
|
118
|
+
# Format result for LLM - handles both text and binary (image/PDF) content
|
|
119
|
+
# This method is called by the agent to format tool results before sending to LLM
|
|
120
|
+
def format_result_for_llm(result)
|
|
121
|
+
# For LLM-compatible binary files with base64 data, return as content blocks
|
|
122
|
+
if result[:binary] && result[:base64_data]
|
|
123
|
+
# Create a text description
|
|
124
|
+
description = "File: #{result[:path]}\nType: #{result[:format]}\nSize: #{format_file_size(result[:size_bytes])}"
|
|
125
|
+
|
|
126
|
+
# Add size warning for large files
|
|
127
|
+
if result[:size_bytes] > 5_000_000
|
|
128
|
+
description += "\nWARNING: Large file (>5MB) - may consume significant tokens"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# For images, return both description and image content
|
|
132
|
+
if result[:mime_type]&.start_with?("image/")
|
|
133
|
+
return {
|
|
134
|
+
type: "image",
|
|
135
|
+
path: result[:path],
|
|
136
|
+
format: result[:format],
|
|
137
|
+
size_bytes: result[:size_bytes],
|
|
138
|
+
mime_type: result[:mime_type],
|
|
139
|
+
base64_data: result[:base64_data],
|
|
140
|
+
description: description
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# For PDFs and other binary formats, just return metadata with base64
|
|
145
|
+
return {
|
|
146
|
+
type: "document",
|
|
147
|
+
path: result[:path],
|
|
148
|
+
format: result[:format],
|
|
149
|
+
size_bytes: result[:size_bytes],
|
|
150
|
+
mime_type: result[:mime_type],
|
|
151
|
+
base64_data: result[:base64_data],
|
|
152
|
+
description: description
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# For other cases, return the result as-is (agent will JSON.generate it)
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private def binary_file?(path)
|
|
161
|
+
# Use FileProcessor to detect binary files
|
|
162
|
+
File.open(path, 'rb') do |file|
|
|
163
|
+
sample = file.read(8192) || ""
|
|
164
|
+
Utils::FileProcessor.binary_file?(sample)
|
|
165
|
+
end
|
|
166
|
+
rescue StandardError
|
|
167
|
+
# If we can't read the file, assume it's not binary
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private def handle_binary_file(path)
|
|
172
|
+
# Check if it's a supported format using FileProcessor
|
|
173
|
+
if Utils::FileProcessor.supported_binary_file?(path)
|
|
174
|
+
# Use FileProcessor to convert to base64
|
|
175
|
+
begin
|
|
176
|
+
result = Utils::FileProcessor.file_to_base64(path)
|
|
177
|
+
{
|
|
178
|
+
path: path,
|
|
179
|
+
binary: true,
|
|
180
|
+
format: result[:format],
|
|
181
|
+
mime_type: result[:mime_type],
|
|
182
|
+
size_bytes: result[:size_bytes],
|
|
183
|
+
base64_data: result[:base64_data],
|
|
184
|
+
error: nil
|
|
185
|
+
}
|
|
186
|
+
rescue ArgumentError => e
|
|
187
|
+
# File too large or other error
|
|
188
|
+
file_size = File.size(path)
|
|
189
|
+
ext = File.extname(path).downcase
|
|
190
|
+
{
|
|
191
|
+
path: path,
|
|
192
|
+
binary: true,
|
|
193
|
+
format: ext.empty? ? "unknown" : ext[1..-1],
|
|
194
|
+
size_bytes: file_size,
|
|
195
|
+
content: nil,
|
|
196
|
+
error: e.message
|
|
197
|
+
}
|
|
198
|
+
end
|
|
199
|
+
else
|
|
200
|
+
# Binary file that we can't send to LLM
|
|
201
|
+
file_size = File.size(path)
|
|
202
|
+
ext = File.extname(path).downcase
|
|
203
|
+
{
|
|
204
|
+
path: path,
|
|
205
|
+
binary: true,
|
|
206
|
+
format: ext.empty? ? "unknown" : ext[1..-1],
|
|
207
|
+
size_bytes: file_size,
|
|
208
|
+
content: nil,
|
|
209
|
+
error: "Binary file detected. This format cannot be read as text. File size: #{format_file_size(file_size)}"
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private def detect_mime_type(path, data)
|
|
215
|
+
Utils::FileProcessor.detect_mime_type(path, data)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private def format_file_size(bytes)
|
|
219
|
+
if bytes < 1024
|
|
220
|
+
"#{bytes} bytes"
|
|
221
|
+
elsif bytes < 1024 * 1024
|
|
222
|
+
"#{(bytes / 1024.0).round(2)} KB"
|
|
223
|
+
else
|
|
224
|
+
"#{(bytes / (1024.0 * 1024)).round(2)} MB"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
94
227
|
|
|
95
228
|
private
|
|
96
229
|
|
|
97
230
|
# List first-level directory contents (files and directories)
|
|
98
|
-
def list_directory_contents(path)
|
|
231
|
+
private def list_directory_contents(path)
|
|
99
232
|
begin
|
|
100
233
|
entries = Dir.entries(path).reject { |entry| entry == "." || entry == ".." }
|
|
101
234
|
|