openclacky 0.5.4 → 0.5.5
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/lib/clacky/agent.rb +34 -16
- data/lib/clacky/cli.rb +48 -131
- data/lib/clacky/client.rb +53 -7
- data/lib/clacky/model_pricing.rb +280 -0
- data/lib/clacky/tools/grep.rb +30 -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 +61 -164
- 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: cc218b590b40d1a2301ef91a73b5a4ded7d6891da5b42eea62f4fa8bce200292
|
|
4
|
+
data.tar.gz: e828ac9fd724d0097f7557401127c513f35138c31d76a06e07c8c70db049cb1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 91db158259b243438e5af2fa26d257f30c4c5f755383e4bde6e20b71cf9e5481bc5b2d91cd5c63e5916ed8d6e373050e60dd16fd4b3a0231073702b75ad2702a
|
|
7
|
+
data.tar.gz: '0940f53a61a33f22de700c8609ede85e7d937f096688374eedef072758372f00e87fa6327050d323a7f5f3d869b487ff39bb942fe56534afe4a906b3b6cde494'
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -9,13 +9,7 @@ require_relative "utils/arguments_parser"
|
|
|
9
9
|
module Clacky
|
|
10
10
|
class Agent
|
|
11
11
|
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
|
|
12
|
+
:cache_stats, :cost_source
|
|
19
13
|
|
|
20
14
|
# System prompt for the coding agent
|
|
21
15
|
SYSTEM_PROMPT = <<~PROMPT.freeze
|
|
@@ -76,6 +70,8 @@ module Clacky
|
|
|
76
70
|
@working_dir = working_dir || Dir.pwd
|
|
77
71
|
@created_at = Time.now.iso8601
|
|
78
72
|
@total_tasks = 0
|
|
73
|
+
@cost_source = :estimated # Track whether cost is from API or estimated
|
|
74
|
+
@task_cost_source = :estimated # Track cost source for current task
|
|
79
75
|
|
|
80
76
|
# Register built-in tools
|
|
81
77
|
register_builtin_tools
|
|
@@ -133,6 +129,7 @@ module Clacky
|
|
|
133
129
|
|
|
134
130
|
def run(user_input, &block)
|
|
135
131
|
@start_time = Time.now
|
|
132
|
+
@task_cost_source = :estimated # Reset for new task
|
|
136
133
|
|
|
137
134
|
# Add system prompt as the first message if this is the first run
|
|
138
135
|
if @messages.empty?
|
|
@@ -221,14 +218,14 @@ module Clacky
|
|
|
221
218
|
# @param status [Symbol] Status of the last task: :success, :error, or :interrupted
|
|
222
219
|
# @param error_message [String] Error message if status is :error
|
|
223
220
|
def to_session_data(status: :success, error_message: nil)
|
|
224
|
-
# Get
|
|
225
|
-
|
|
221
|
+
# Get last real user message for preview (skip compressed system messages)
|
|
222
|
+
last_user_msg = @messages.reverse.find do |m|
|
|
226
223
|
m[:role] == "user" && !m[:content].to_s.start_with?("[SYSTEM]")
|
|
227
224
|
end
|
|
228
225
|
|
|
229
|
-
# Extract preview text from
|
|
230
|
-
|
|
231
|
-
content =
|
|
226
|
+
# Extract preview text from last user message
|
|
227
|
+
last_message_preview = if last_user_msg
|
|
228
|
+
content = last_user_msg[:content]
|
|
232
229
|
if content.is_a?(String)
|
|
233
230
|
# Truncate to 100 characters for preview
|
|
234
231
|
content.length > 100 ? "#{content[0..100]}..." : content
|
|
@@ -270,7 +267,7 @@ module Clacky
|
|
|
270
267
|
},
|
|
271
268
|
stats: stats_data,
|
|
272
269
|
messages: @messages,
|
|
273
|
-
first_user_message:
|
|
270
|
+
first_user_message: last_message_preview
|
|
274
271
|
}
|
|
275
272
|
end
|
|
276
273
|
|
|
@@ -613,9 +610,29 @@ module Clacky
|
|
|
613
610
|
end
|
|
614
611
|
|
|
615
612
|
def track_cost(usage)
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
613
|
+
# Priority 1: Use API-provided cost if available (OpenRouter, LiteLLM, etc.)
|
|
614
|
+
if usage[:api_cost]
|
|
615
|
+
@total_cost += usage[:api_cost]
|
|
616
|
+
@cost_source = :api
|
|
617
|
+
@task_cost_source = :api
|
|
618
|
+
puts "[DEBUG] Using API-provided cost: $#{usage[:api_cost]}" if @config.verbose
|
|
619
|
+
else
|
|
620
|
+
# Priority 2: Calculate from tokens using ModelPricing
|
|
621
|
+
result = ModelPricing.calculate_cost(model: @config.model, usage: usage)
|
|
622
|
+
cost = result[:cost]
|
|
623
|
+
pricing_source = result[:source]
|
|
624
|
+
|
|
625
|
+
@total_cost += cost
|
|
626
|
+
# Map pricing source to cost source: :price or :default
|
|
627
|
+
@cost_source = pricing_source
|
|
628
|
+
@task_cost_source = pricing_source
|
|
629
|
+
|
|
630
|
+
if @config.verbose
|
|
631
|
+
source_label = pricing_source == :price ? "model pricing" : "default pricing"
|
|
632
|
+
puts "[DEBUG] Calculated cost for #{@config.model} using #{source_label}: $#{cost.round(6)}"
|
|
633
|
+
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}"
|
|
634
|
+
end
|
|
635
|
+
end
|
|
619
636
|
|
|
620
637
|
# Track cache usage statistics
|
|
621
638
|
@cache_stats[:total_requests] += 1
|
|
@@ -1081,6 +1098,7 @@ module Clacky
|
|
|
1081
1098
|
iterations: @iterations,
|
|
1082
1099
|
duration_seconds: Time.now - @start_time,
|
|
1083
1100
|
total_cost_usd: @total_cost.round(4),
|
|
1101
|
+
cost_source: @task_cost_source, # Add cost source for this task
|
|
1084
1102
|
cache_stats: @cache_stats,
|
|
1085
1103
|
messages: @messages,
|
|
1086
1104
|
error: error
|
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
|
|
|
@@ -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
|
|
@@ -408,8 +386,8 @@ module Clacky
|
|
|
408
386
|
cost: total_cost
|
|
409
387
|
)
|
|
410
388
|
|
|
411
|
-
# Use enhanced prompt with "
|
|
412
|
-
result = prompt.read_input(prefix: "
|
|
389
|
+
# Use enhanced prompt with "❯" prefix
|
|
390
|
+
result = prompt.read_input(prefix: "❯")
|
|
413
391
|
|
|
414
392
|
# EnhancedPrompt returns { text: String, images: Array } or nil
|
|
415
393
|
# For now, we only use the text part
|
|
@@ -442,6 +420,7 @@ module Clacky
|
|
|
442
420
|
cost: result[:total_cost_usd].round(4),
|
|
443
421
|
total_tasks: total_tasks,
|
|
444
422
|
total_cost: total_cost.round(4),
|
|
423
|
+
cost_source: result[:cost_source],
|
|
445
424
|
cache_stats: result[:cache_stats]
|
|
446
425
|
)
|
|
447
426
|
rescue Clacky::AgentInterrupted
|
|
@@ -632,68 +611,6 @@ module Clacky
|
|
|
632
611
|
@pastel ||= Pastel.new
|
|
633
612
|
end
|
|
634
613
|
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
614
|
end
|
|
698
615
|
|
|
699
616
|
class ConfigCommand < Thor
|
data/lib/clacky/client.rb
CHANGED
|
@@ -55,12 +55,28 @@ module Clacky
|
|
|
55
55
|
# Add tools if provided
|
|
56
56
|
# For Claude API with caching: mark the last tool definition with cache_control
|
|
57
57
|
if tools&.any?
|
|
58
|
-
|
|
58
|
+
caching_supported = supports_prompt_caching?(model)
|
|
59
|
+
caching_enabled = enable_caching && caching_supported
|
|
60
|
+
|
|
61
|
+
# Debug logging for caching decisions
|
|
62
|
+
if verbose || ENV["CLACKY_DEBUG"]
|
|
63
|
+
puts "\n[DEBUG] Prompt Caching Analysis:"
|
|
64
|
+
puts " Model: #{model}"
|
|
65
|
+
puts " Caching Requested: #{enable_caching}"
|
|
66
|
+
puts " Caching Supported: #{caching_supported}"
|
|
67
|
+
puts " Caching Enabled: #{caching_enabled}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if caching_enabled
|
|
59
71
|
# Deep clone tools to avoid modifying original
|
|
60
72
|
cached_tools = tools.map { |tool| deep_clone(tool) }
|
|
61
73
|
# Mark the last tool for caching (Claude caches from cache breakpoint to end)
|
|
62
74
|
cached_tools.last[:cache_control] = { type: "ephemeral" }
|
|
63
75
|
body[:tools] = cached_tools
|
|
76
|
+
|
|
77
|
+
if verbose || ENV["CLACKY_DEBUG"]
|
|
78
|
+
puts " Cache Control Added: Last tool marked for caching"
|
|
79
|
+
end
|
|
64
80
|
else
|
|
65
81
|
body[:tools] = tools
|
|
66
82
|
end
|
|
@@ -91,14 +107,28 @@ module Clacky
|
|
|
91
107
|
private
|
|
92
108
|
|
|
93
109
|
# Check if the model supports prompt caching
|
|
94
|
-
# Currently only Claude 3.5
|
|
110
|
+
# Currently only Claude 3.5+ models support this feature
|
|
95
111
|
def supports_prompt_caching?(model)
|
|
96
112
|
model_str = model.to_s.downcase
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
model_str.include?("claude
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
|
|
114
|
+
# Only Claude models support prompt caching
|
|
115
|
+
return false unless model_str.include?("claude")
|
|
116
|
+
|
|
117
|
+
# Pattern matching for supported Claude versions:
|
|
118
|
+
# - claude-3.5-*, claude-3-5-*, claude-3.5.*
|
|
119
|
+
# - claude-3.7-*, claude-3-7-*, claude-3.7.*
|
|
120
|
+
# - claude-4*, claude-sonnet-4*
|
|
121
|
+
# - anthropic/claude-sonnet-4* (OpenRouter format)
|
|
122
|
+
cache_pattern = /
|
|
123
|
+
claude # Must contain "claude"
|
|
124
|
+
(?: # Non-capturing group for version patterns
|
|
125
|
+
(?:-3[-.]?[5-9])| # 3.5, 3.6, 3.7, 3.8, 3.9 or 3-5, 3-6, etc
|
|
126
|
+
(?:-[4-9])| # 4, 5, 6, 7, 8, 9 (future versions)
|
|
127
|
+
(?:-sonnet-[34]) # OpenRouter: claude-sonnet-3, claude-sonnet-4
|
|
128
|
+
)
|
|
129
|
+
/x
|
|
130
|
+
|
|
131
|
+
model_str.match?(cache_pattern)
|
|
102
132
|
end
|
|
103
133
|
|
|
104
134
|
# Deep clone a hash/array structure (for tool definitions)
|
|
@@ -162,6 +192,11 @@ module Clacky
|
|
|
162
192
|
total_tokens: usage["total_tokens"]
|
|
163
193
|
}
|
|
164
194
|
|
|
195
|
+
# Add OpenRouter cost information if present
|
|
196
|
+
if usage["cost"]
|
|
197
|
+
usage_data[:api_cost] = usage["cost"]
|
|
198
|
+
end
|
|
199
|
+
|
|
165
200
|
# Add cache metrics if present (Claude API with prompt caching)
|
|
166
201
|
if usage["cache_creation_input_tokens"]
|
|
167
202
|
usage_data[:cache_creation_input_tokens] = usage["cache_creation_input_tokens"]
|
|
@@ -170,6 +205,17 @@ module Clacky
|
|
|
170
205
|
usage_data[:cache_read_input_tokens] = usage["cache_read_input_tokens"]
|
|
171
206
|
end
|
|
172
207
|
|
|
208
|
+
# Add OpenRouter cache information from prompt_tokens_details
|
|
209
|
+
if usage["prompt_tokens_details"]
|
|
210
|
+
details = usage["prompt_tokens_details"]
|
|
211
|
+
if details["cached_tokens"] && details["cached_tokens"] > 0
|
|
212
|
+
usage_data[:cache_read_input_tokens] = details["cached_tokens"]
|
|
213
|
+
end
|
|
214
|
+
if details["cache_write_tokens"] && details["cache_write_tokens"] > 0
|
|
215
|
+
usage_data[:cache_creation_input_tokens] = details["cache_write_tokens"]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
173
219
|
{
|
|
174
220
|
content: message["content"],
|
|
175
221
|
tool_calls: parse_tool_calls(message["tool_calls"]),
|