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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f86dc057b8fd69db07c40a5633ffe1350cf0ddb8509b6e26705464d9e127daa4
4
- data.tar.gz: 58d3e6b89e129d6c7ef7dbc57ff57e95ee8d6acd96186491daa8670b6031c3b2
3
+ metadata.gz: cc218b590b40d1a2301ef91a73b5a4ded7d6891da5b42eea62f4fa8bce200292
4
+ data.tar.gz: e828ac9fd724d0097f7557401127c513f35138c31d76a06e07c8c70db049cb1c
5
5
  SHA512:
6
- metadata.gz: 4d724c3d9404faed0bead1fb3e417782f15a9d89dde03709696120df87230d7a6d33250684e96e99f13b87f3993bfffd53dc5b52c87c34e79844a171429685af
7
- data.tar.gz: 837b07953b38515d0c1bf0b6081a1fd10e7fffa46f9cbf1d1721744cabe494c5a018bcb1d0290e988782a534aad9ab24137978d7299d1908cb9fb248258de282
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 first real user message for preview (skip compressed system messages)
225
- first_user_msg = @messages.find do |m|
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 first user message
230
- first_message_preview = if first_user_msg
231
- content = first_user_msg[: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: first_message_preview
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
- input_cost = (usage[:prompt_tokens] / 1_000_000.0) * PRICING[:input]
617
- output_cost = (usage[:completion_tokens] / 1_000_000.0) * PRICING[:output]
618
- @total_cost += input_cost + output_cost
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
- desc "chat [MESSAGE]", "Start a chat with Claude or send a single message"
18
- long_desc <<-LONGDESC
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 "tools", "List available tools"
163
- option :category, type: :string, desc: "Filter by category"
164
- def tools
165
- registry = ToolRegistry.new
166
-
167
- registry.register(Tools::Shell.new)
168
- registry.register(Tools::FileReader.new)
169
- registry.register(Tools::Write.new)
170
- registry.register(Tools::Edit.new)
171
- registry.register(Tools::Glob.new)
172
- registry.register(Tools::Grep.new)
173
- registry.register(Tools::WebSearch.new)
174
- registry.register(Tools::WebFetch.new)
175
-
176
- say "\n📦 Available Tools:\n\n", :green
177
-
178
- tools_to_show = if options[:category]
179
- registry.by_category(options[:category])
180
- else
181
- registry.all
182
- end
183
-
184
- tools_to_show.each do |tool|
185
- say " #{tool.name}", :cyan
186
- say " #{tool.description}", :white
187
- say " Category: #{tool.category}", :yellow
188
-
189
- if tool.parameters[:properties]
190
- say " Parameters:", :yellow
191
- tool.parameters[:properties].each do |name, spec|
192
- required = tool.parameters[:required]&.include?(name.to_s) ? " (required)" : ""
193
- say " - #{name}: #{spec[:description]}#{required}", :white
194
- end
195
- end
196
- say ""
197
- end
198
-
199
- say "Total: #{tools_to_show.size} tools\n", :green
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 "You:" prefix
412
- result = prompt.read_input(prefix: "You:")
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
- if enable_caching && supports_prompt_caching?(model)
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 Sonnet and newer Claude models support this
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
- # Claude 3.5 Sonnet (20241022 and newer) supports prompt caching
98
- # Also Claude 3.7 Sonnet and Opus models when they're released
99
- model_str.include?("claude-3.5-sonnet") ||
100
- model_str.include?("claude-3-7") ||
101
- model_str.include?("claude-4")
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"]),