openclacky 0.5.1 ā 0.5.3
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 +2 -0
- data/README.md +1 -0
- data/lib/clacky/agent.rb +121 -17
- data/lib/clacky/agent_config.rb +4 -1
- data/lib/clacky/cli.rb +119 -93
- data/lib/clacky/client.rb +56 -7
- data/lib/clacky/hook_manager.rb +2 -1
- data/lib/clacky/progress_indicator.rb +13 -10
- data/lib/clacky/tools/safe_shell.rb +7 -2
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/ui/banner.rb +144 -0
- data/lib/clacky/ui/formatter.rb +209 -0
- data/lib/clacky/ui/prompt.rb +72 -0
- data/lib/clacky/ui/statusbar.rb +98 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +6 -0
- metadata +33 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52ab39c40c69d821b45d01ed1d14086a50446309421891e9cd6724a9b32864cf
|
|
4
|
+
data.tar.gz: 0d61ef02a23f8a04157f8b7b2308a4f0c68fd37bba1407894b9841cd707c6873
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d654bd29a093bf23f1e0c162867251e08a28ec722deaae7a9d234f2aed913329d991df29391778612c458c51b6775881ee0867cb2d959c0f7072b14e662d0c38
|
|
7
|
+
data.tar.gz: 8056dff60d1c845751021e2c852e58071360c8d6a6b81cec72be238e647521ae36a211d72af88602239f8abcab5a15b7a345b8efd98e68b62a0a6dfde8a63b23
|
data/.clackyrules
CHANGED
|
@@ -22,7 +22,9 @@ It provides chat functionality and autonomous AI agent capabilities with tool us
|
|
|
22
22
|
- Use frozen string literals: `# frozen_string_literal: true`
|
|
23
23
|
- Keep classes focused and single-responsibility
|
|
24
24
|
- Use meaningful variable and method names
|
|
25
|
+
- **IMPORTANT**: All code comments must be written in English
|
|
25
26
|
- Add descriptive comments for complex logic
|
|
27
|
+
- Use clear, self-documenting code with English naming
|
|
26
28
|
|
|
27
29
|
### Architecture Patterns
|
|
28
30
|
- Tools inherit from `Clacky::Tools::Base`
|
data/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A command-line interface for interacting with AI models. OpenClacky supports Ope
|
|
|
12
12
|
- šØ Colorful terminal output
|
|
13
13
|
- š OpenAI-compatible API support (OpenAI, Gitee AI, DeepSeek, etc.)
|
|
14
14
|
- š ļø Rich built-in tools: file operations, web search, code execution, and more
|
|
15
|
+
- ā” Prompt caching support for Claude models (reduces costs up to 90%)
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "json"
|
|
5
|
-
require "
|
|
5
|
+
require "tty-prompt"
|
|
6
6
|
require "set"
|
|
7
7
|
require_relative "utils/arguments_parser"
|
|
8
8
|
|
|
9
9
|
module Clacky
|
|
10
10
|
class Agent
|
|
11
|
-
attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos
|
|
11
|
+
attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
12
|
+
:cache_stats
|
|
12
13
|
|
|
13
14
|
# Pricing per 1M tokens (approximate - adjust based on actual model)
|
|
14
15
|
PRICING = {
|
|
@@ -65,6 +66,12 @@ module Clacky
|
|
|
65
66
|
@todos = [] # Store todos in memory
|
|
66
67
|
@iterations = 0
|
|
67
68
|
@total_cost = 0.0
|
|
69
|
+
@cache_stats = {
|
|
70
|
+
cache_creation_input_tokens: 0,
|
|
71
|
+
cache_read_input_tokens: 0,
|
|
72
|
+
total_requests: 0,
|
|
73
|
+
cache_hit_requests: 0
|
|
74
|
+
}
|
|
68
75
|
@start_time = nil
|
|
69
76
|
@working_dir = working_dir || Dir.pwd
|
|
70
77
|
@created_at = Time.now.iso8601
|
|
@@ -91,6 +98,14 @@ module Clacky
|
|
|
91
98
|
@created_at = session_data[:created_at]
|
|
92
99
|
@total_tasks = session_data.dig(:stats, :total_tasks) || 0
|
|
93
100
|
|
|
101
|
+
# Restore cache statistics if available
|
|
102
|
+
@cache_stats = session_data.dig(:stats, :cache_stats) || {
|
|
103
|
+
cache_creation_input_tokens: 0,
|
|
104
|
+
cache_read_input_tokens: 0,
|
|
105
|
+
total_requests: 0,
|
|
106
|
+
cache_hit_requests: 0
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
# Check if the session ended with an error
|
|
95
110
|
last_status = session_data.dig(:stats, :last_status)
|
|
96
111
|
last_error = session_data.dig(:stats, :last_error)
|
|
@@ -103,7 +118,7 @@ module Clacky
|
|
|
103
118
|
@messages = @messages[0...last_user_index]
|
|
104
119
|
|
|
105
120
|
# Trigger a hook to notify about the rollback
|
|
106
|
-
|
|
121
|
+
@hooks.trigger(:session_rollback, {
|
|
107
122
|
reason: "Previous session ended with error",
|
|
108
123
|
error_message: last_error,
|
|
109
124
|
rolled_back_message_index: last_user_index
|
|
@@ -122,7 +137,14 @@ module Clacky
|
|
|
122
137
|
# Add system prompt as the first message if this is the first run
|
|
123
138
|
if @messages.empty?
|
|
124
139
|
system_prompt = build_system_prompt
|
|
125
|
-
|
|
140
|
+
system_message = { role: "system", content: system_prompt }
|
|
141
|
+
|
|
142
|
+
# Enable caching for system prompt if configured and model supports it
|
|
143
|
+
if @config.enable_prompt_caching
|
|
144
|
+
system_message[:cache_control] = { type: "ephemeral" }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@messages << system_message
|
|
126
148
|
end
|
|
127
149
|
|
|
128
150
|
@messages << { role: "user", content: user_input }
|
|
@@ -222,7 +244,8 @@ module Clacky
|
|
|
222
244
|
total_iterations: @iterations,
|
|
223
245
|
total_cost_usd: @total_cost.round(4),
|
|
224
246
|
duration_seconds: @start_time ? (Time.now - @start_time).round(2) : 0,
|
|
225
|
-
last_status: status.to_s
|
|
247
|
+
last_status: status.to_s,
|
|
248
|
+
cache_stats: @cache_stats
|
|
226
249
|
}
|
|
227
250
|
|
|
228
251
|
# Add error message if status is error
|
|
@@ -240,6 +263,7 @@ module Clacky
|
|
|
240
263
|
max_iterations: @config.max_iterations,
|
|
241
264
|
max_cost_usd: @config.max_cost_usd,
|
|
242
265
|
enable_compression: @config.enable_compression,
|
|
266
|
+
enable_prompt_caching: @config.enable_prompt_caching,
|
|
243
267
|
keep_recent_messages: @config.keep_recent_messages,
|
|
244
268
|
max_tokens: @config.max_tokens,
|
|
245
269
|
verbose: @config.verbose
|
|
@@ -366,7 +390,8 @@ module Clacky
|
|
|
366
390
|
model: @config.model,
|
|
367
391
|
tools: tools_to_send,
|
|
368
392
|
max_tokens: @config.max_tokens,
|
|
369
|
-
verbose: @config.verbose
|
|
393
|
+
verbose: @config.verbose,
|
|
394
|
+
enable_caching: @config.enable_prompt_caching
|
|
370
395
|
)
|
|
371
396
|
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
372
397
|
retries += 1
|
|
@@ -386,6 +411,54 @@ module Clacky
|
|
|
386
411
|
|
|
387
412
|
track_cost(response[:usage])
|
|
388
413
|
|
|
414
|
+
# Handle truncated responses (when max_tokens limit is reached)
|
|
415
|
+
if response[:finish_reason] == "length"
|
|
416
|
+
# Count recent truncations to prevent infinite loops
|
|
417
|
+
recent_truncations = @messages.last(5).count { |m|
|
|
418
|
+
m[:role] == "user" && m[:content]&.include?("[SYSTEM] Your response was truncated")
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if recent_truncations >= 2
|
|
422
|
+
# Too many truncations - task is too complex
|
|
423
|
+
progress.finish
|
|
424
|
+
puts "\nā ļø Response truncated multiple times. Task is too complex for a single response." if @config.verbose
|
|
425
|
+
|
|
426
|
+
# Create a response that tells the user to break down the task
|
|
427
|
+
error_response = {
|
|
428
|
+
content: "I apologize, but this task is too complex to complete in a single response. " \
|
|
429
|
+
"Please break it down into smaller steps, or reduce the amount of content to generate at once.\n\n" \
|
|
430
|
+
"For example, when creating a long document:\n" \
|
|
431
|
+
"1. First create the file with a basic structure\n" \
|
|
432
|
+
"2. Then use edit() to add content section by section",
|
|
433
|
+
finish_reason: "stop",
|
|
434
|
+
tool_calls: nil
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Add this as an assistant message so it appears in conversation
|
|
438
|
+
@messages << {
|
|
439
|
+
role: "assistant",
|
|
440
|
+
content: error_response[:content]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return error_response
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Insert system message to guide LLM to retry with smaller steps
|
|
447
|
+
@messages << {
|
|
448
|
+
role: "user",
|
|
449
|
+
content: "[SYSTEM] Your response was truncated due to length limit. Please retry with a different approach:\n" \
|
|
450
|
+
"- For long file content: create the file with structure first, then use edit() to add content section by section\n" \
|
|
451
|
+
"- Break down large tasks into multiple smaller steps\n" \
|
|
452
|
+
"- Avoid putting more than 2000 characters in a single tool call argument\n" \
|
|
453
|
+
"- Use multiple tool calls instead of one large call"
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
puts "ā ļø Response truncated due to length limit. Retrying with smaller steps..." if @config.verbose
|
|
457
|
+
|
|
458
|
+
# Recursively retry
|
|
459
|
+
return think(&block)
|
|
460
|
+
end
|
|
461
|
+
|
|
389
462
|
# Add assistant response to messages
|
|
390
463
|
msg = { role: "assistant" }
|
|
391
464
|
# Always include content field (some APIs require it even with tool_calls)
|
|
@@ -543,6 +616,18 @@ module Clacky
|
|
|
543
616
|
input_cost = (usage[:prompt_tokens] / 1_000_000.0) * PRICING[:input]
|
|
544
617
|
output_cost = (usage[:completion_tokens] / 1_000_000.0) * PRICING[:output]
|
|
545
618
|
@total_cost += input_cost + output_cost
|
|
619
|
+
|
|
620
|
+
# Track cache usage statistics
|
|
621
|
+
@cache_stats[:total_requests] += 1
|
|
622
|
+
|
|
623
|
+
if usage[:cache_creation_input_tokens]
|
|
624
|
+
@cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
if usage[:cache_read_input_tokens]
|
|
628
|
+
@cache_stats[:cache_read_input_tokens] += usage[:cache_read_input_tokens]
|
|
629
|
+
@cache_stats[:cache_hit_requests] += 1
|
|
630
|
+
end
|
|
546
631
|
end
|
|
547
632
|
|
|
548
633
|
def compress_messages_if_needed
|
|
@@ -582,7 +667,15 @@ module Clacky
|
|
|
582
667
|
summary = summarize_messages(messages_to_compress)
|
|
583
668
|
|
|
584
669
|
# Rebuild messages array: [system, summary, recent_messages]
|
|
585
|
-
|
|
670
|
+
# Preserve cache_control on system message if it exists
|
|
671
|
+
rebuilt_messages = [system_msg, summary, *recent_messages].compact
|
|
672
|
+
|
|
673
|
+
# Re-apply cache control to system message if caching is enabled
|
|
674
|
+
if @config.enable_prompt_caching && rebuilt_messages.first&.dig(:role) == "system"
|
|
675
|
+
rebuilt_messages.first[:cache_control] = { type: "ephemeral" }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
@messages = rebuilt_messages
|
|
586
679
|
|
|
587
680
|
final_size = @messages.size
|
|
588
681
|
|
|
@@ -610,7 +703,7 @@ module Clacky
|
|
|
610
703
|
|
|
611
704
|
# Track which messages to include
|
|
612
705
|
messages_to_include = Set.new
|
|
613
|
-
|
|
706
|
+
|
|
614
707
|
# Start from the end and work backwards
|
|
615
708
|
i = messages.size - 1
|
|
616
709
|
messages_collected = 0
|
|
@@ -631,13 +724,13 @@ module Clacky
|
|
|
631
724
|
# If this is an assistant message with tool_calls, we MUST include ALL corresponding tool results
|
|
632
725
|
if msg[:role] == "assistant" && msg[:tool_calls]
|
|
633
726
|
tool_call_ids = msg[:tool_calls].map { |tc| tc[:id] }
|
|
634
|
-
|
|
727
|
+
|
|
635
728
|
# Find all tool results that belong to this assistant message
|
|
636
729
|
# They should be in the messages immediately following this assistant message
|
|
637
730
|
j = i + 1
|
|
638
731
|
while j < messages.size
|
|
639
732
|
next_msg = messages[j]
|
|
640
|
-
|
|
733
|
+
|
|
641
734
|
# If we find a tool result for one of our tool_calls, include it
|
|
642
735
|
if next_msg[:role] == "tool" && tool_call_ids.include?(next_msg[:tool_call_id])
|
|
643
736
|
messages_to_include.add(j)
|
|
@@ -645,7 +738,7 @@ module Clacky
|
|
|
645
738
|
# Stop when we hit a non-tool message (start of next turn)
|
|
646
739
|
break
|
|
647
740
|
end
|
|
648
|
-
|
|
741
|
+
|
|
649
742
|
j += 1
|
|
650
743
|
end
|
|
651
744
|
end
|
|
@@ -766,18 +859,28 @@ module Clacky
|
|
|
766
859
|
prompt_text = format_tool_prompt(call)
|
|
767
860
|
puts "\nā #{prompt_text}"
|
|
768
861
|
|
|
769
|
-
# Use
|
|
770
|
-
|
|
862
|
+
# Use TTY::Prompt for better input handling
|
|
863
|
+
tty_prompt = TTY::Prompt.new(interrupt: :exit)
|
|
771
864
|
|
|
772
|
-
|
|
865
|
+
begin
|
|
866
|
+
response = tty_prompt.ask(" (Enter/y to approve, n to deny, or provide feedback):", required: false) do |q|
|
|
867
|
+
q.modify :strip
|
|
868
|
+
end
|
|
869
|
+
rescue TTY::Reader::InputInterrupt
|
|
870
|
+
# Handle Ctrl+C
|
|
871
|
+
puts
|
|
773
872
|
return { approved: false, feedback: nil }
|
|
774
873
|
end
|
|
775
874
|
|
|
776
|
-
response
|
|
875
|
+
# Handle nil response (EOF/pipe input)
|
|
876
|
+
if response.nil? || response.empty?
|
|
877
|
+
return { approved: true, feedback: nil } # Empty means approved
|
|
878
|
+
end
|
|
879
|
+
|
|
777
880
|
response_lower = response.downcase
|
|
778
881
|
|
|
779
|
-
#
|
|
780
|
-
if
|
|
882
|
+
# "y"/"yes" = approved
|
|
883
|
+
if response_lower == "y" || response_lower == "yes"
|
|
781
884
|
return { approved: true, feedback: nil }
|
|
782
885
|
end
|
|
783
886
|
|
|
@@ -978,6 +1081,7 @@ module Clacky
|
|
|
978
1081
|
iterations: @iterations,
|
|
979
1082
|
duration_seconds: Time.now - @start_time,
|
|
980
1083
|
total_cost_usd: @total_cost.round(4),
|
|
1084
|
+
cache_stats: @cache_stats,
|
|
981
1085
|
messages: @messages,
|
|
982
1086
|
error: error
|
|
983
1087
|
}
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Clacky
|
|
|
7
7
|
|
|
8
8
|
attr_accessor :model, :max_iterations, :max_cost_usd, :timeout_seconds,
|
|
9
9
|
:permission_mode, :allowed_tools, :disallowed_tools,
|
|
10
|
-
:max_tokens, :verbose, :enable_compression, :keep_recent_messages
|
|
10
|
+
:max_tokens, :verbose, :enable_compression, :keep_recent_messages,
|
|
11
|
+
:enable_prompt_caching
|
|
11
12
|
|
|
12
13
|
def initialize(options = {})
|
|
13
14
|
@model = options[:model] || "gpt-3.5-turbo"
|
|
@@ -21,6 +22,8 @@ module Clacky
|
|
|
21
22
|
@verbose = options[:verbose] || false
|
|
22
23
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
23
24
|
@keep_recent_messages = options[:keep_recent_messages] || 20
|
|
25
|
+
# Enable prompt caching by default for cost savings
|
|
26
|
+
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
|