openclacky 0.5.4 → 0.5.6
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/.clackyrules +4 -0
- data/README.md +1 -1
- data/lib/clacky/agent.rb +190 -19
- data/lib/clacky/cli.rb +126 -141
- data/lib/clacky/client.rb +53 -7
- data/lib/clacky/model_pricing.rb +280 -0
- data/lib/clacky/progress_indicator.rb +1 -1
- data/lib/clacky/tools/file_reader.rb +73 -10
- data/lib/clacky/tools/grep.rb +74 -7
- data/lib/clacky/tools/safe_shell.rb +9 -4
- data/lib/clacky/tools/shell.rb +60 -22
- data/lib/clacky/ui/banner.rb +22 -11
- data/lib/clacky/ui/enhanced_prompt.rb +366 -223
- data/lib/clacky/ui/formatter.rb +1 -1
- data/lib/clacky/ui/statusbar.rb +0 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +1 -1
- metadata +2 -2
- data/lib/clacky/conversation.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34f843ea474327ea95f88685111f1a86149730503625c5e971de239e63764969
|
|
4
|
+
data.tar.gz: 85f2e93d791d3f774aa4f0d29177d4999fd91306f3c441f21d89c11ab37f15d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8bcb13d8cb5ee790ff698fc9bd0be4d2b366e1d16216db6a2f24e56c0dc1954c3b82349473b49cdee7c66ae732e3b684ee1d249d54dabe0f7d9cb0cf1f59873b
|
|
7
|
+
data.tar.gz: 866e043424ca831364baba5a842b5554cc1da2309e2935a0458edbead75c5e89f9cdf146506df12fc04c87d78c5613e3b2e812b13a816864af5861b1f815c788
|
data/.clackyrules
CHANGED
|
@@ -25,6 +25,7 @@ It provides chat functionality and autonomous AI agent capabilities with tool us
|
|
|
25
25
|
- **IMPORTANT**: All code comments must be written in English
|
|
26
26
|
- Add descriptive comments for complex logic
|
|
27
27
|
- Use clear, self-documenting code with English naming
|
|
28
|
+
- **IMPORTANT**: Always use inline `private` with method definitions (e.g., `private def method_name`). Do NOT use standalone `private` keyword
|
|
28
29
|
|
|
29
30
|
### Architecture Patterns
|
|
30
31
|
- Tools inherit from `Clacky::Tools::Base`
|
|
@@ -41,6 +42,9 @@ It provides chat functionality and autonomous AI agent capabilities with tool us
|
|
|
41
42
|
- Maintain good test coverage
|
|
42
43
|
- When modifying existing functionality, ensure related tests still pass
|
|
43
44
|
- When adding new features, consider adding corresponding tests
|
|
45
|
+
- **IMPORTANT**: When developing new features, write RSpec tests as needed and ensure they pass
|
|
46
|
+
- **DO NOT** write custom test scripts unless explicitly requested by the user
|
|
47
|
+
- **DO NOT** create markdown documentation unless explicitly requested by the user
|
|
44
48
|
|
|
45
49
|
### Tool Development
|
|
46
50
|
When adding new tools:
|
data/README.md
CHANGED
data/lib/clacky/agent.rb
CHANGED
|
@@ -4,18 +4,13 @@ require "securerandom"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "tty-prompt"
|
|
6
6
|
require "set"
|
|
7
|
+
require "base64"
|
|
7
8
|
require_relative "utils/arguments_parser"
|
|
8
9
|
|
|
9
10
|
module Clacky
|
|
10
11
|
class Agent
|
|
11
12
|
attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
12
|
-
:cache_stats
|
|
13
|
-
|
|
14
|
-
# Pricing per 1M tokens (approximate - adjust based on actual model)
|
|
15
|
-
PRICING = {
|
|
16
|
-
input: 0.50, # $0.50 per 1M input tokens
|
|
17
|
-
output: 1.50 # $1.50 per 1M output tokens
|
|
18
|
-
}.freeze
|
|
13
|
+
:cache_stats, :cost_source
|
|
19
14
|
|
|
20
15
|
# System prompt for the coding agent
|
|
21
16
|
SYSTEM_PROMPT = <<~PROMPT.freeze
|
|
@@ -76,6 +71,9 @@ module Clacky
|
|
|
76
71
|
@working_dir = working_dir || Dir.pwd
|
|
77
72
|
@created_at = Time.now.iso8601
|
|
78
73
|
@total_tasks = 0
|
|
74
|
+
@cost_source = :estimated # Track whether cost is from API or estimated
|
|
75
|
+
@task_cost_source = :estimated # Track cost source for current task
|
|
76
|
+
@previous_total_tokens = 0 # Track tokens from previous iteration for delta calculation
|
|
79
77
|
|
|
80
78
|
# Register built-in tools
|
|
81
79
|
register_builtin_tools
|
|
@@ -131,8 +129,10 @@ module Clacky
|
|
|
131
129
|
@hooks.add(event, &block)
|
|
132
130
|
end
|
|
133
131
|
|
|
134
|
-
def run(user_input, &block)
|
|
132
|
+
def run(user_input, images: [], &block)
|
|
135
133
|
@start_time = Time.now
|
|
134
|
+
@task_cost_source = :estimated # Reset for new task
|
|
135
|
+
@previous_total_tokens = 0 # Reset token tracking for new task
|
|
136
136
|
|
|
137
137
|
# Add system prompt as the first message if this is the first run
|
|
138
138
|
if @messages.empty?
|
|
@@ -147,7 +147,9 @@ module Clacky
|
|
|
147
147
|
@messages << system_message
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
# Format user message with images if provided
|
|
151
|
+
user_content = format_user_content(user_input, images)
|
|
152
|
+
@messages << { role: "user", content: user_content }
|
|
151
153
|
@total_tasks += 1
|
|
152
154
|
|
|
153
155
|
emit_event(:on_start, { input: user_input }, &block)
|
|
@@ -221,14 +223,14 @@ module Clacky
|
|
|
221
223
|
# @param status [Symbol] Status of the last task: :success, :error, or :interrupted
|
|
222
224
|
# @param error_message [String] Error message if status is :error
|
|
223
225
|
def to_session_data(status: :success, error_message: nil)
|
|
224
|
-
# Get
|
|
225
|
-
|
|
226
|
+
# Get last real user message for preview (skip compressed system messages)
|
|
227
|
+
last_user_msg = @messages.reverse.find do |m|
|
|
226
228
|
m[:role] == "user" && !m[:content].to_s.start_with?("[SYSTEM]")
|
|
227
229
|
end
|
|
228
230
|
|
|
229
|
-
# Extract preview text from
|
|
230
|
-
|
|
231
|
-
content =
|
|
231
|
+
# Extract preview text from last user message
|
|
232
|
+
last_message_preview = if last_user_msg
|
|
233
|
+
content = last_user_msg[:content]
|
|
232
234
|
if content.is_a?(String)
|
|
233
235
|
# Truncate to 100 characters for preview
|
|
234
236
|
content.length > 100 ? "#{content[0..100]}..." : content
|
|
@@ -270,7 +272,7 @@ module Clacky
|
|
|
270
272
|
},
|
|
271
273
|
stats: stats_data,
|
|
272
274
|
messages: @messages,
|
|
273
|
-
first_user_message:
|
|
275
|
+
first_user_message: last_message_preview
|
|
274
276
|
}
|
|
275
277
|
end
|
|
276
278
|
|
|
@@ -613,9 +615,35 @@ module Clacky
|
|
|
613
615
|
end
|
|
614
616
|
|
|
615
617
|
def track_cost(usage)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
618
|
+
# Priority 1: Use API-provided cost if available (OpenRouter, LiteLLM, etc.)
|
|
619
|
+
iteration_cost = nil
|
|
620
|
+
if usage[:api_cost]
|
|
621
|
+
@total_cost += usage[:api_cost]
|
|
622
|
+
@cost_source = :api
|
|
623
|
+
@task_cost_source = :api
|
|
624
|
+
iteration_cost = usage[:api_cost]
|
|
625
|
+
puts "[DEBUG] Using API-provided cost: $#{usage[:api_cost]}" if @config.verbose
|
|
626
|
+
else
|
|
627
|
+
# Priority 2: Calculate from tokens using ModelPricing
|
|
628
|
+
result = ModelPricing.calculate_cost(model: @config.model, usage: usage)
|
|
629
|
+
cost = result[:cost]
|
|
630
|
+
pricing_source = result[:source]
|
|
631
|
+
|
|
632
|
+
@total_cost += cost
|
|
633
|
+
iteration_cost = cost
|
|
634
|
+
# Map pricing source to cost source: :price or :default
|
|
635
|
+
@cost_source = pricing_source
|
|
636
|
+
@task_cost_source = pricing_source
|
|
637
|
+
|
|
638
|
+
if @config.verbose
|
|
639
|
+
source_label = pricing_source == :price ? "model pricing" : "default pricing"
|
|
640
|
+
puts "[DEBUG] Calculated cost for #{@config.model} using #{source_label}: $#{cost.round(6)}"
|
|
641
|
+
puts "[DEBUG] Usage breakdown: prompt=#{usage[:prompt_tokens]}, completion=#{usage[:completion_tokens]}, cache_write=#{usage[:cache_creation_input_tokens] || 0}, cache_read=#{usage[:cache_read_input_tokens] || 0}"
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Display token usage statistics for this iteration
|
|
646
|
+
display_iteration_tokens(usage, iteration_cost)
|
|
619
647
|
|
|
620
648
|
# Track cache usage statistics
|
|
621
649
|
@cache_stats[:total_requests] += 1
|
|
@@ -630,6 +658,66 @@ module Clacky
|
|
|
630
658
|
end
|
|
631
659
|
end
|
|
632
660
|
|
|
661
|
+
# Display token usage for current iteration
|
|
662
|
+
private def display_iteration_tokens(usage, cost)
|
|
663
|
+
prompt_tokens = usage[:prompt_tokens] || 0
|
|
664
|
+
completion_tokens = usage[:completion_tokens] || 0
|
|
665
|
+
total_tokens = usage[:total_tokens] || (prompt_tokens + completion_tokens)
|
|
666
|
+
cache_write = usage[:cache_creation_input_tokens] || 0
|
|
667
|
+
cache_read = usage[:cache_read_input_tokens] || 0
|
|
668
|
+
|
|
669
|
+
# Calculate token delta from previous iteration
|
|
670
|
+
delta_tokens = total_tokens - @previous_total_tokens
|
|
671
|
+
@previous_total_tokens = total_tokens # Update for next iteration
|
|
672
|
+
|
|
673
|
+
# Build token summary string
|
|
674
|
+
token_info = []
|
|
675
|
+
|
|
676
|
+
# Delta tokens with color coding at the beginning
|
|
677
|
+
require 'pastel'
|
|
678
|
+
pastel = Pastel.new
|
|
679
|
+
|
|
680
|
+
delta_str = "+#{delta_tokens}"
|
|
681
|
+
colored_delta = if delta_tokens > 10000
|
|
682
|
+
pastel.red.bold(delta_str) # Error level: red for > 10k
|
|
683
|
+
elsif delta_tokens > 5000
|
|
684
|
+
pastel.yellow.bold(delta_str) # Warn level: yellow for > 5k
|
|
685
|
+
else
|
|
686
|
+
pastel.green(delta_str) # Normal: green for <= 5k
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
token_info << colored_delta
|
|
690
|
+
|
|
691
|
+
# Cache status indicator
|
|
692
|
+
cache_used = cache_read > 0 || cache_write > 0
|
|
693
|
+
if cache_used
|
|
694
|
+
cache_indicator = "✓ Cached"
|
|
695
|
+
token_info << pastel.cyan(cache_indicator)
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
# Input tokens (with cache breakdown if available)
|
|
699
|
+
if cache_write > 0 || cache_read > 0
|
|
700
|
+
input_detail = "#{prompt_tokens} (cache: #{cache_read} read, #{cache_write} write)"
|
|
701
|
+
token_info << "Input: #{input_detail}"
|
|
702
|
+
else
|
|
703
|
+
token_info << "Input: #{prompt_tokens}"
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# Output tokens
|
|
707
|
+
token_info << "Output: #{completion_tokens}"
|
|
708
|
+
|
|
709
|
+
# Total
|
|
710
|
+
token_info << "Total: #{total_tokens}"
|
|
711
|
+
|
|
712
|
+
# Cost for this iteration
|
|
713
|
+
if cost
|
|
714
|
+
token_info << "Cost: $#{cost.round(6)}"
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Display with color
|
|
718
|
+
puts pastel.dim(" [Tokens] #{token_info.join(' | ')}")
|
|
719
|
+
end
|
|
720
|
+
|
|
633
721
|
def compress_messages_if_needed
|
|
634
722
|
# Check if compression is enabled
|
|
635
723
|
return unless @config.enable_compression
|
|
@@ -1038,9 +1126,20 @@ module Clacky
|
|
|
1038
1126
|
end
|
|
1039
1127
|
|
|
1040
1128
|
def build_success_result(call, result)
|
|
1129
|
+
# Try to get tool instance to use its format_result_for_llm method
|
|
1130
|
+
tool = @tool_registry.get(call[:name]) rescue nil
|
|
1131
|
+
|
|
1132
|
+
formatted_result = if tool && tool.respond_to?(:format_result_for_llm)
|
|
1133
|
+
# Tool provides a custom LLM-friendly format
|
|
1134
|
+
tool.format_result_for_llm(result)
|
|
1135
|
+
else
|
|
1136
|
+
# Fallback: use the original result
|
|
1137
|
+
result
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1041
1140
|
{
|
|
1042
1141
|
id: call[:id],
|
|
1043
|
-
content: JSON.generate(
|
|
1142
|
+
content: JSON.generate(formatted_result)
|
|
1044
1143
|
}
|
|
1045
1144
|
end
|
|
1046
1145
|
|
|
@@ -1081,6 +1180,7 @@ module Clacky
|
|
|
1081
1180
|
iterations: @iterations,
|
|
1082
1181
|
duration_seconds: Time.now - @start_time,
|
|
1083
1182
|
total_cost_usd: @total_cost.round(4),
|
|
1183
|
+
cost_source: @task_cost_source, # Add cost source for this task
|
|
1084
1184
|
cache_stats: @cache_stats,
|
|
1085
1185
|
messages: @messages,
|
|
1086
1186
|
error: error
|
|
@@ -1115,5 +1215,76 @@ module Clacky
|
|
|
1115
1215
|
@tool_registry.register(Tools::TodoManager.new)
|
|
1116
1216
|
@tool_registry.register(Tools::RunProject.new)
|
|
1117
1217
|
end
|
|
1218
|
+
|
|
1219
|
+
# Format user content with optional images
|
|
1220
|
+
# @param text [String] User's text input
|
|
1221
|
+
# @param images [Array<String>] Array of image file paths
|
|
1222
|
+
# @return [String|Array] String if no images, Array with text and image_url objects if images present
|
|
1223
|
+
def format_user_content(text, images)
|
|
1224
|
+
return text if images.nil? || images.empty?
|
|
1225
|
+
|
|
1226
|
+
content = []
|
|
1227
|
+
content << { type: "text", text: text } unless text.nil? || text.empty?
|
|
1228
|
+
|
|
1229
|
+
images.each do |image_path|
|
|
1230
|
+
image_url = image_path_to_data_url(image_path)
|
|
1231
|
+
content << { type: "image_url", image_url: { url: image_url } }
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
content
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Convert image file path to base64 data URL
|
|
1238
|
+
# @param path [String] File path to image
|
|
1239
|
+
# @return [String] base64 data URL (e.g., "data:image/png;base64,...")
|
|
1240
|
+
def image_path_to_data_url(path)
|
|
1241
|
+
unless File.exist?(path)
|
|
1242
|
+
raise ArgumentError, "Image file not found: #{path}"
|
|
1243
|
+
end
|
|
1244
|
+
|
|
1245
|
+
# Read file as binary
|
|
1246
|
+
image_data = File.binread(path)
|
|
1247
|
+
|
|
1248
|
+
# Detect MIME type from file extension or content
|
|
1249
|
+
mime_type = detect_image_mime_type(path, image_data)
|
|
1250
|
+
|
|
1251
|
+
# Encode to base64
|
|
1252
|
+
base64_data = Base64.strict_encode64(image_data)
|
|
1253
|
+
|
|
1254
|
+
"data:#{mime_type};base64,#{base64_data}"
|
|
1255
|
+
end
|
|
1256
|
+
|
|
1257
|
+
# Detect image MIME type
|
|
1258
|
+
# @param path [String] File path
|
|
1259
|
+
# @param data [String] Binary image data
|
|
1260
|
+
# @return [String] MIME type (e.g., "image/png")
|
|
1261
|
+
def detect_image_mime_type(path, data)
|
|
1262
|
+
# Try to detect from file extension first
|
|
1263
|
+
ext = File.extname(path).downcase
|
|
1264
|
+
case ext
|
|
1265
|
+
when ".png"
|
|
1266
|
+
"image/png"
|
|
1267
|
+
when ".jpg", ".jpeg"
|
|
1268
|
+
"image/jpeg"
|
|
1269
|
+
when ".gif"
|
|
1270
|
+
"image/gif"
|
|
1271
|
+
when ".webp"
|
|
1272
|
+
"image/webp"
|
|
1273
|
+
else
|
|
1274
|
+
# Try to detect from file signature (magic bytes)
|
|
1275
|
+
if data.start_with?("\x89PNG".b)
|
|
1276
|
+
"image/png"
|
|
1277
|
+
elsif data.start_with?("\xFF\xD8\xFF".b)
|
|
1278
|
+
"image/jpeg"
|
|
1279
|
+
elsif data.start_with?("GIF87a".b) || data.start_with?("GIF89a".b)
|
|
1280
|
+
"image/gif"
|
|
1281
|
+
elsif data.start_with?("RIFF".b) && data[8..11] == "WEBP".b
|
|
1282
|
+
"image/webp"
|
|
1283
|
+
else
|
|
1284
|
+
# Default to png if unknown
|
|
1285
|
+
"image/png"
|
|
1286
|
+
end
|
|
1287
|
+
end
|
|
1288
|
+
end
|
|
1118
1289
|
end
|
|
1119
1290
|
end
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -14,41 +14,15 @@ module Clacky
|
|
|
14
14
|
true
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Start an interactive chat session with Claude AI.
|
|
20
|
-
|
|
21
|
-
If MESSAGE is provided, send it as a single message and exit.
|
|
22
|
-
If no MESSAGE is provided, start an interactive chat session.
|
|
23
|
-
|
|
24
|
-
Examples:
|
|
25
|
-
$ clacky chat "What is Ruby?"
|
|
26
|
-
$ clacky chat
|
|
27
|
-
LONGDESC
|
|
28
|
-
option :model, type: :string, desc: "Model to use (default from config)"
|
|
29
|
-
def chat(message = nil)
|
|
30
|
-
config = Clacky::Config.load
|
|
31
|
-
|
|
32
|
-
unless config.api_key
|
|
33
|
-
say "Error: API key not found. Please run 'clacky config set' first.", :red
|
|
34
|
-
exit 1
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
if message
|
|
38
|
-
# Single message mode
|
|
39
|
-
send_single_message(message, config)
|
|
40
|
-
else
|
|
41
|
-
# Interactive mode
|
|
42
|
-
start_interactive_chat(config)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
17
|
+
# Set agent as the default command
|
|
18
|
+
default_task :agent
|
|
45
19
|
|
|
46
20
|
desc "version", "Show clacky version"
|
|
47
21
|
def version
|
|
48
22
|
say "Clacky version #{Clacky::VERSION}"
|
|
49
23
|
end
|
|
50
24
|
|
|
51
|
-
desc "agent [MESSAGE]", "Run agent in interactive mode with autonomous tool use"
|
|
25
|
+
desc "agent [MESSAGE]", "Run agent in interactive mode with autonomous tool use (default)"
|
|
52
26
|
long_desc <<-LONGDESC
|
|
53
27
|
Run an AI agent in interactive mode that can autonomously use tools to complete tasks.
|
|
54
28
|
|
|
@@ -133,7 +107,7 @@ module Clacky
|
|
|
133
107
|
|
|
134
108
|
begin
|
|
135
109
|
# Always run in interactive mode
|
|
136
|
-
run_agent_interactive(agent, working_dir, agent_config, message, session_manager)
|
|
110
|
+
run_agent_interactive(agent, working_dir, agent_config, message, session_manager, client)
|
|
137
111
|
rescue StandardError => e
|
|
138
112
|
# Save session on error
|
|
139
113
|
if session_manager
|
|
@@ -159,44 +133,48 @@ module Clacky
|
|
|
159
133
|
end
|
|
160
134
|
end
|
|
161
135
|
|
|
162
|
-
desc "
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
say "
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
say "
|
|
136
|
+
desc "price", "Show pricing information for AI models"
|
|
137
|
+
def price
|
|
138
|
+
say "\n💰 Model Pricing Information\n\n", :green
|
|
139
|
+
|
|
140
|
+
say "Clacky supports three pricing modes when calculating API costs:\n\n", :white
|
|
141
|
+
|
|
142
|
+
say " 1. ", :cyan
|
|
143
|
+
say "API-provided cost", :bold
|
|
144
|
+
say " (", :white
|
|
145
|
+
say ":api", :yellow
|
|
146
|
+
say ")", :white
|
|
147
|
+
say "\n The most accurate - uses actual cost data from the API response", :white
|
|
148
|
+
say "\n Supported by: OpenRouter, LiteLLM, and other compatible proxies\n\n"
|
|
149
|
+
|
|
150
|
+
say " 2. ", :cyan
|
|
151
|
+
say "Model-specific pricing", :bold
|
|
152
|
+
say " (", :white
|
|
153
|
+
say ":price", :yellow
|
|
154
|
+
say ")", :white
|
|
155
|
+
say "\n Uses official pricing from model providers (Claude models)", :white
|
|
156
|
+
say "\n Includes tiered pricing and prompt caching discounts\n\n"
|
|
157
|
+
|
|
158
|
+
say " 3. ", :cyan
|
|
159
|
+
say "Default fallback pricing", :bold
|
|
160
|
+
say " (", :white
|
|
161
|
+
say ":default", :yellow
|
|
162
|
+
say ")", :white
|
|
163
|
+
say "\n Conservative estimates for unknown models", :white
|
|
164
|
+
say "\n Input: $0.50/MTok, Output: $1.50/MTok\n\n"
|
|
165
|
+
|
|
166
|
+
say "Priority order: API cost > Model pricing > Default pricing\n\n", :yellow
|
|
167
|
+
|
|
168
|
+
say "Supported models with official pricing:\n", :green
|
|
169
|
+
say " • claude-opus-4.5\n", :cyan
|
|
170
|
+
say " • claude-sonnet-4.5\n", :cyan
|
|
171
|
+
say " • claude-haiku-4.5\n", :cyan
|
|
172
|
+
say " • claude-3-5-sonnet-20241022\n", :cyan
|
|
173
|
+
say " • claude-3-5-sonnet-20240620\n", :cyan
|
|
174
|
+
say " • claude-3-5-haiku-20241022\n\n", :cyan
|
|
175
|
+
|
|
176
|
+
say "For detailed pricing information, visit:\n", :white
|
|
177
|
+
say "https://www.anthropic.com/pricing\n\n", :blue
|
|
200
178
|
end
|
|
201
179
|
|
|
202
180
|
no_commands do
|
|
@@ -353,7 +331,7 @@ module Clacky
|
|
|
353
331
|
end
|
|
354
332
|
end
|
|
355
333
|
|
|
356
|
-
def run_agent_interactive(agent, working_dir, agent_config, initial_message = nil, session_manager = nil)
|
|
334
|
+
def run_agent_interactive(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil)
|
|
357
335
|
# Store agent as instance variable for access in display methods
|
|
358
336
|
@current_agent = agent
|
|
359
337
|
|
|
@@ -393,10 +371,13 @@ module Clacky
|
|
|
393
371
|
|
|
394
372
|
# Process initial message if provided
|
|
395
373
|
current_message = initial_message
|
|
374
|
+
current_images = []
|
|
396
375
|
|
|
397
376
|
loop do
|
|
398
377
|
# Get message from user if not provided
|
|
399
378
|
unless current_message && !current_message.strip.empty?
|
|
379
|
+
# Only show newline separator if we've completed tasks
|
|
380
|
+
# (but not right after /clear since we just showed a message)
|
|
400
381
|
say "\n" if total_tasks > 0
|
|
401
382
|
|
|
402
383
|
# Show status bar before input
|
|
@@ -408,24 +389,87 @@ module Clacky
|
|
|
408
389
|
cost: total_cost
|
|
409
390
|
)
|
|
410
391
|
|
|
411
|
-
# Use enhanced prompt with "
|
|
412
|
-
result = prompt.read_input(prefix: "
|
|
392
|
+
# Use enhanced prompt with "❯" prefix
|
|
393
|
+
result = prompt.read_input(prefix: "❯") do |display_lines|
|
|
394
|
+
# Shift+Tab pressed - toggle mode and update status bar
|
|
395
|
+
if agent_config.permission_mode == :confirm_safes
|
|
396
|
+
agent_config.permission_mode = :auto_approve
|
|
397
|
+
else
|
|
398
|
+
agent_config.permission_mode = :confirm_safes
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
# Update status bar (it's above the input box)
|
|
402
|
+
# display_lines includes the final newline, so we need display_lines moves to reach status bar
|
|
403
|
+
print "\e[#{display_lines}A" # Move up to status bar line
|
|
404
|
+
print "\r\e[2K" # Clear the status bar line
|
|
405
|
+
|
|
406
|
+
# Redisplay status bar with new mode (puts adds newline, cursor moves to next line)
|
|
407
|
+
statusbar.display(
|
|
408
|
+
working_dir: working_dir,
|
|
409
|
+
mode: agent_config.permission_mode.to_s,
|
|
410
|
+
model: agent_config.model,
|
|
411
|
+
tasks: total_tasks,
|
|
412
|
+
cost: total_cost
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Move back down to original position (display_lines - 1 because puts moved us down 1)
|
|
416
|
+
print "\e[#{display_lines - 1}B"
|
|
417
|
+
end
|
|
413
418
|
|
|
414
|
-
# EnhancedPrompt returns
|
|
415
|
-
#
|
|
416
|
-
|
|
419
|
+
# EnhancedPrompt returns:
|
|
420
|
+
# - { text: String, images: Array } for normal input
|
|
421
|
+
# - { command: Symbol } for commands
|
|
422
|
+
# - nil on EOF
|
|
423
|
+
if result.nil?
|
|
424
|
+
current_message = nil
|
|
425
|
+
current_images = []
|
|
426
|
+
break
|
|
427
|
+
elsif result[:command]
|
|
428
|
+
# Handle commands
|
|
429
|
+
case result[:command]
|
|
430
|
+
when :clear
|
|
431
|
+
# Clear session by creating a new agent
|
|
432
|
+
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir)
|
|
433
|
+
@current_agent = agent
|
|
434
|
+
total_tasks = 0
|
|
435
|
+
total_cost = 0.0
|
|
436
|
+
ui_formatter.info("Session cleared. Starting fresh.")
|
|
437
|
+
current_message = nil
|
|
438
|
+
current_images = []
|
|
439
|
+
next
|
|
440
|
+
when :exit
|
|
441
|
+
current_message = nil
|
|
442
|
+
current_images = []
|
|
443
|
+
break
|
|
444
|
+
end
|
|
445
|
+
else
|
|
446
|
+
# Normal input with text and optional images
|
|
447
|
+
current_message = result[:text]
|
|
448
|
+
current_images = result[:images] || []
|
|
449
|
+
end
|
|
417
450
|
|
|
418
451
|
break if current_message.nil? || %w[exit quit].include?(current_message&.downcase&.strip)
|
|
419
|
-
next if current_message.strip.empty?
|
|
452
|
+
next if current_message.strip.empty? && current_images.empty?
|
|
420
453
|
|
|
421
454
|
# Display user's message after input
|
|
422
455
|
ui_formatter.user_message(current_message)
|
|
456
|
+
|
|
457
|
+
# Display image info if images were pasted (without extra newline)
|
|
458
|
+
if current_images.any?
|
|
459
|
+
current_images.each_with_index do |img_path, idx|
|
|
460
|
+
filename = File.basename(img_path)
|
|
461
|
+
say " 📎 Image #{idx + 1}: #{filename}", :cyan
|
|
462
|
+
end
|
|
463
|
+
puts # Add newline after all images
|
|
464
|
+
else
|
|
465
|
+
puts # Add newline after user message if no images
|
|
466
|
+
end
|
|
423
467
|
end
|
|
424
468
|
|
|
425
469
|
total_tasks += 1
|
|
426
470
|
|
|
427
471
|
begin
|
|
428
|
-
result = agent.run(current_message) do |event|
|
|
472
|
+
result = agent.run(current_message, images: current_images) do |event|
|
|
429
473
|
display_agent_event(event)
|
|
430
474
|
end
|
|
431
475
|
|
|
@@ -442,6 +486,7 @@ module Clacky
|
|
|
442
486
|
cost: result[:total_cost_usd].round(4),
|
|
443
487
|
total_tasks: total_tasks,
|
|
444
488
|
total_cost: total_cost.round(4),
|
|
489
|
+
cost_source: result[:cost_source],
|
|
445
490
|
cache_stats: result[:cache_stats]
|
|
446
491
|
)
|
|
447
492
|
rescue Clacky::AgentInterrupted
|
|
@@ -470,12 +515,14 @@ module Clacky
|
|
|
470
515
|
say "\nOr you can continue with a new task or type 'exit' to quit.", :yellow
|
|
471
516
|
end
|
|
472
517
|
|
|
473
|
-
# Clear current_message to prompt for next input
|
|
518
|
+
# Clear current_message and current_images to prompt for next input
|
|
474
519
|
current_message = nil
|
|
520
|
+
current_images = []
|
|
475
521
|
end
|
|
476
522
|
|
|
477
|
-
# Save final session state
|
|
478
|
-
|
|
523
|
+
# Save final session state only if there were actual tasks
|
|
524
|
+
# Don't save empty sessions where user just started and exited
|
|
525
|
+
if session_manager && total_tasks > 0
|
|
479
526
|
session_manager.save(agent.to_session_data)
|
|
480
527
|
end
|
|
481
528
|
|
|
@@ -632,68 +679,6 @@ module Clacky
|
|
|
632
679
|
@pastel ||= Pastel.new
|
|
633
680
|
end
|
|
634
681
|
end
|
|
635
|
-
|
|
636
|
-
private
|
|
637
|
-
|
|
638
|
-
def send_single_message(message, config)
|
|
639
|
-
spinner = TTY::Spinner.new("[:spinner] Thinking...", format: :dots)
|
|
640
|
-
spinner.auto_spin
|
|
641
|
-
|
|
642
|
-
client = Clacky::Client.new(config.api_key, base_url: config.base_url)
|
|
643
|
-
response = client.send_message(message, model: options[:model] || config.model)
|
|
644
|
-
|
|
645
|
-
spinner.success("Done!")
|
|
646
|
-
say "\n#{response}", :cyan
|
|
647
|
-
rescue StandardError => e
|
|
648
|
-
spinner.error("Failed!")
|
|
649
|
-
say "Error: #{e.message}", :red
|
|
650
|
-
exit 1
|
|
651
|
-
end
|
|
652
|
-
|
|
653
|
-
def start_interactive_chat(config)
|
|
654
|
-
say "Starting interactive chat with Claude...", :green
|
|
655
|
-
say "Type 'exit' or 'quit' to end the session.\n\n", :yellow
|
|
656
|
-
|
|
657
|
-
conversation = Clacky::Conversation.new(
|
|
658
|
-
config.api_key,
|
|
659
|
-
model: options[:model] || config.model,
|
|
660
|
-
base_url: config.base_url
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
# Use TTY::Prompt for input
|
|
664
|
-
tty_prompt = TTY::Prompt.new(interrupt: :exit)
|
|
665
|
-
|
|
666
|
-
loop do
|
|
667
|
-
# Use TTY::Prompt for better input handling
|
|
668
|
-
begin
|
|
669
|
-
message = tty_prompt.ask("You:", required: false) do |q|
|
|
670
|
-
q.modify :strip
|
|
671
|
-
end
|
|
672
|
-
rescue TTY::Reader::InputInterrupt
|
|
673
|
-
# Handle Ctrl+C
|
|
674
|
-
puts
|
|
675
|
-
break
|
|
676
|
-
end
|
|
677
|
-
|
|
678
|
-
break if message.nil? || %w[exit quit].include?(message&.downcase&.strip)
|
|
679
|
-
next if message.nil? || message.strip.empty?
|
|
680
|
-
|
|
681
|
-
spinner = TTY::Spinner.new("[:spinner] Claude is thinking...", format: :dots)
|
|
682
|
-
spinner.auto_spin
|
|
683
|
-
|
|
684
|
-
begin
|
|
685
|
-
response = conversation.send_message(message)
|
|
686
|
-
spinner.success("Claude:")
|
|
687
|
-
say response, :cyan
|
|
688
|
-
say "\n"
|
|
689
|
-
rescue StandardError => e
|
|
690
|
-
spinner.error("Error!")
|
|
691
|
-
say "Error: #{e.message}", :red
|
|
692
|
-
end
|
|
693
|
-
end
|
|
694
|
-
|
|
695
|
-
say "\nGoodbye!", :green
|
|
696
|
-
end
|
|
697
682
|
end
|
|
698
683
|
|
|
699
684
|
class ConfigCommand < Thor
|