openclacky 0.5.2 → 0.5.4
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 +13 -0
- data/README.md +2 -0
- data/Rakefile +1 -5
- data/lib/clacky/agent.rb +29 -19
- data/lib/clacky/cli.rb +23 -9
- data/lib/clacky/gitignore_parser.rb +114 -0
- data/lib/clacky/progress_indicator.rb +1 -0
- data/lib/clacky/tools/grep.rb +245 -30
- data/lib/clacky/ui/enhanced_prompt.rb +643 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +2 -1
- metadata +3 -2
- data/lib/clacky/ui/prompt.rb +0 -70
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f86dc057b8fd69db07c40a5633ffe1350cf0ddb8509b6e26705464d9e127daa4
|
|
4
|
+
data.tar.gz: 58d3e6b89e129d6c7ef7dbc57ff57e95ee8d6acd96186491daa8670b6031c3b2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d724c3d9404faed0bead1fb3e417782f15a9d89dde03709696120df87230d7a6d33250684e96e99f13b87f3993bfffd53dc5b52c87c34e79844a171429685af
|
|
7
|
+
data.tar.gz: 837b07953b38515d0c1bf0b6081a1fd10e7fffa46f9cbf1d1721744cabe494c5a018bcb1d0290e988782a534aad9ab24137978d7299d1908cb9fb248258de282
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.4] - 2026-01-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Automatic Paste Detection**: Rapid input detection automatically identifies paste operations
|
|
14
|
+
- **Word Wrap Display**: Long input lines automatically wrap with scroll indicators (up to 15 visible lines)
|
|
15
|
+
- **Full-width Terminal Display**: Enhanced prompt box uses full terminal width for better visibility
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
- **Smart Ctrl+C Handling**: First press clears content, second press (within 2s) exits
|
|
19
|
+
- **UTF-8 Encoding**: Better handling of multi-byte characters in clipboard operations
|
|
20
|
+
- **Cursor Positioning**: Improved cursor tracking in wrapped lines
|
|
21
|
+
- **Multi-line Paste**: Better display for pasted content with placeholder support
|
|
22
|
+
|
|
10
23
|
## [0.5.0] - 2026-01-11
|
|
11
24
|
|
|
12
25
|
### Added
|
data/README.md
CHANGED
|
@@ -6,6 +6,8 @@ A command-line interface for interacting with AI models. OpenClacky supports Ope
|
|
|
6
6
|
|
|
7
7
|
- 💬 Interactive chat sessions with AI models
|
|
8
8
|
- 🤖 Autonomous AI agent with tool use capabilities
|
|
9
|
+
- 📝 Enhanced input with multi-line support and Unicode (Chinese, etc.)
|
|
10
|
+
- 🖼️ Paste images from clipboard (macOS/Linux)
|
|
9
11
|
- 🚀 Single-message mode for quick queries
|
|
10
12
|
- 🔐 Secure API key management
|
|
11
13
|
- 📝 Multi-turn conversation support
|
data/Rakefile
CHANGED
|
@@ -5,10 +5,6 @@ require "rspec/core/rake_task"
|
|
|
5
5
|
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
|
7
7
|
|
|
8
|
-
require "rubocop/rake_task"
|
|
9
|
-
|
|
10
|
-
RuboCop::RakeTask.new
|
|
11
|
-
|
|
12
8
|
namespace :build do
|
|
13
9
|
desc "Build both openclacky and clacky gems"
|
|
14
10
|
task :all do
|
|
@@ -35,4 +31,4 @@ namespace :build do
|
|
|
35
31
|
end
|
|
36
32
|
end
|
|
37
33
|
|
|
38
|
-
task default: %i[spec
|
|
34
|
+
task default: %i[spec]
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
|
@@ -97,7 +97,7 @@ module Clacky
|
|
|
97
97
|
@working_dir = session_data[:working_dir]
|
|
98
98
|
@created_at = session_data[:created_at]
|
|
99
99
|
@total_tasks = session_data.dig(:stats, :total_tasks) || 0
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
# Restore cache statistics if available
|
|
102
102
|
@cache_stats = session_data.dig(:stats, :cache_stats) || {
|
|
103
103
|
cache_creation_input_tokens: 0,
|
|
@@ -138,12 +138,12 @@ module Clacky
|
|
|
138
138
|
if @messages.empty?
|
|
139
139
|
system_prompt = build_system_prompt
|
|
140
140
|
system_message = { role: "system", content: system_prompt }
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
# Enable caching for system prompt if configured and model supports it
|
|
143
143
|
if @config.enable_prompt_caching
|
|
144
144
|
system_message[:cache_control] = { type: "ephemeral" }
|
|
145
145
|
end
|
|
146
|
-
|
|
146
|
+
|
|
147
147
|
@messages << system_message
|
|
148
148
|
end
|
|
149
149
|
|
|
@@ -616,14 +616,14 @@ module Clacky
|
|
|
616
616
|
input_cost = (usage[:prompt_tokens] / 1_000_000.0) * PRICING[:input]
|
|
617
617
|
output_cost = (usage[:completion_tokens] / 1_000_000.0) * PRICING[:output]
|
|
618
618
|
@total_cost += input_cost + output_cost
|
|
619
|
-
|
|
619
|
+
|
|
620
620
|
# Track cache usage statistics
|
|
621
621
|
@cache_stats[:total_requests] += 1
|
|
622
|
-
|
|
622
|
+
|
|
623
623
|
if usage[:cache_creation_input_tokens]
|
|
624
624
|
@cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
|
|
625
625
|
end
|
|
626
|
-
|
|
626
|
+
|
|
627
627
|
if usage[:cache_read_input_tokens]
|
|
628
628
|
@cache_stats[:cache_read_input_tokens] += usage[:cache_read_input_tokens]
|
|
629
629
|
@cache_stats[:cache_hit_requests] += 1
|
|
@@ -669,12 +669,12 @@ module Clacky
|
|
|
669
669
|
# Rebuild messages array: [system, summary, recent_messages]
|
|
670
670
|
# Preserve cache_control on system message if it exists
|
|
671
671
|
rebuilt_messages = [system_msg, summary, *recent_messages].compact
|
|
672
|
-
|
|
672
|
+
|
|
673
673
|
# Re-apply cache control to system message if caching is enabled
|
|
674
674
|
if @config.enable_prompt_caching && rebuilt_messages.first&.dig(:role) == "system"
|
|
675
675
|
rebuilt_messages.first[:cache_control] = { type: "ephemeral" }
|
|
676
676
|
end
|
|
677
|
-
|
|
677
|
+
|
|
678
678
|
@messages = rebuilt_messages
|
|
679
679
|
|
|
680
680
|
final_size = @messages.size
|
|
@@ -703,7 +703,7 @@ module Clacky
|
|
|
703
703
|
|
|
704
704
|
# Track which messages to include
|
|
705
705
|
messages_to_include = Set.new
|
|
706
|
-
|
|
706
|
+
|
|
707
707
|
# Start from the end and work backwards
|
|
708
708
|
i = messages.size - 1
|
|
709
709
|
messages_collected = 0
|
|
@@ -724,13 +724,13 @@ module Clacky
|
|
|
724
724
|
# If this is an assistant message with tool_calls, we MUST include ALL corresponding tool results
|
|
725
725
|
if msg[:role] == "assistant" && msg[:tool_calls]
|
|
726
726
|
tool_call_ids = msg[:tool_calls].map { |tc| tc[:id] }
|
|
727
|
-
|
|
727
|
+
|
|
728
728
|
# Find all tool results that belong to this assistant message
|
|
729
729
|
# They should be in the messages immediately following this assistant message
|
|
730
730
|
j = i + 1
|
|
731
731
|
while j < messages.size
|
|
732
732
|
next_msg = messages[j]
|
|
733
|
-
|
|
733
|
+
|
|
734
734
|
# If we find a tool result for one of our tool_calls, include it
|
|
735
735
|
if next_msg[:role] == "tool" && tool_call_ids.include?(next_msg[:tool_call_id])
|
|
736
736
|
messages_to_include.add(j)
|
|
@@ -738,7 +738,7 @@ module Clacky
|
|
|
738
738
|
# Stop when we hit a non-tool message (start of next turn)
|
|
739
739
|
break
|
|
740
740
|
end
|
|
741
|
-
|
|
741
|
+
|
|
742
742
|
j += 1
|
|
743
743
|
end
|
|
744
744
|
end
|
|
@@ -859,18 +859,28 @@ module Clacky
|
|
|
859
859
|
prompt_text = format_tool_prompt(call)
|
|
860
860
|
puts "\n❓ #{prompt_text}"
|
|
861
861
|
|
|
862
|
-
# Use
|
|
863
|
-
|
|
862
|
+
# Use TTY::Prompt for better input handling
|
|
863
|
+
tty_prompt = TTY::Prompt.new(interrupt: :exit)
|
|
864
864
|
|
|
865
|
-
|
|
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
|
|
866
872
|
return { approved: false, feedback: nil }
|
|
867
873
|
end
|
|
868
874
|
|
|
869
|
-
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
|
+
|
|
870
880
|
response_lower = response.downcase
|
|
871
881
|
|
|
872
|
-
#
|
|
873
|
-
if
|
|
882
|
+
# "y"/"yes" = approved
|
|
883
|
+
if response_lower == "y" || response_lower == "yes"
|
|
874
884
|
return { approved: true, feedback: nil }
|
|
875
885
|
end
|
|
876
886
|
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "tty-prompt"
|
|
5
5
|
require "tty-spinner"
|
|
6
|
-
require "readline"
|
|
7
6
|
require_relative "ui/banner"
|
|
8
|
-
require_relative "ui/
|
|
7
|
+
require_relative "ui/enhanced_prompt"
|
|
9
8
|
require_relative "ui/statusbar"
|
|
10
9
|
require_relative "ui/formatter"
|
|
11
10
|
|
|
@@ -410,11 +409,15 @@ module Clacky
|
|
|
410
409
|
)
|
|
411
410
|
|
|
412
411
|
# Use enhanced prompt with "You:" prefix
|
|
413
|
-
|
|
412
|
+
result = prompt.read_input(prefix: "You:")
|
|
413
|
+
|
|
414
|
+
# EnhancedPrompt returns { text: String, images: Array } or nil
|
|
415
|
+
# For now, we only use the text part
|
|
416
|
+
current_message = result.nil? ? nil : result[:text]
|
|
414
417
|
|
|
415
418
|
break if current_message.nil? || %w[exit quit].include?(current_message&.downcase&.strip)
|
|
416
419
|
next if current_message.strip.empty?
|
|
417
|
-
|
|
420
|
+
|
|
418
421
|
# Display user's message after input
|
|
419
422
|
ui_formatter.user_message(current_message)
|
|
420
423
|
end
|
|
@@ -614,7 +617,7 @@ module Clacky
|
|
|
614
617
|
end
|
|
615
618
|
|
|
616
619
|
def ui_prompt
|
|
617
|
-
@ui_prompt ||= UI::
|
|
620
|
+
@ui_prompt ||= UI::EnhancedPrompt.new
|
|
618
621
|
end
|
|
619
622
|
|
|
620
623
|
def ui_statusbar
|
|
@@ -657,12 +660,23 @@ module Clacky
|
|
|
657
660
|
base_url: config.base_url
|
|
658
661
|
)
|
|
659
662
|
|
|
663
|
+
# Use TTY::Prompt for input
|
|
664
|
+
tty_prompt = TTY::Prompt.new(interrupt: :exit)
|
|
665
|
+
|
|
660
666
|
loop do
|
|
661
|
-
# Use
|
|
662
|
-
|
|
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
|
|
663
677
|
|
|
664
|
-
break if message.nil? || %w[exit quit].include?(message
|
|
665
|
-
next if message.strip.empty?
|
|
678
|
+
break if message.nil? || %w[exit quit].include?(message&.downcase&.strip)
|
|
679
|
+
next if message.nil? || message.strip.empty?
|
|
666
680
|
|
|
667
681
|
spinner = TTY::Spinner.new("[:spinner] Claude is thinking...", format: :dots)
|
|
668
682
|
spinner.auto_spin
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
# Parser for .gitignore files to determine which files should be ignored
|
|
5
|
+
class GitignoreParser
|
|
6
|
+
attr_reader :patterns
|
|
7
|
+
|
|
8
|
+
def initialize(gitignore_path = nil)
|
|
9
|
+
@patterns = []
|
|
10
|
+
@negation_patterns = []
|
|
11
|
+
|
|
12
|
+
if gitignore_path && File.exist?(gitignore_path)
|
|
13
|
+
parse_gitignore(gitignore_path)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Check if a file path should be ignored
|
|
18
|
+
def ignored?(path)
|
|
19
|
+
relative_path = path.start_with?('./') ? path[2..] : path
|
|
20
|
+
|
|
21
|
+
# Check negation patterns first (! prefix in .gitignore)
|
|
22
|
+
@negation_patterns.each do |pattern|
|
|
23
|
+
return false if match_pattern?(relative_path, pattern)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Then check ignore patterns
|
|
27
|
+
@patterns.each do |pattern|
|
|
28
|
+
return true if match_pattern?(relative_path, pattern)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def parse_gitignore(path)
|
|
37
|
+
File.readlines(path, chomp: true).each do |line|
|
|
38
|
+
# Skip comments and empty lines
|
|
39
|
+
next if line.strip.empty? || line.start_with?('#')
|
|
40
|
+
|
|
41
|
+
# Handle negation patterns (lines starting with !)
|
|
42
|
+
if line.start_with?('!')
|
|
43
|
+
@negation_patterns << normalize_pattern(line[1..])
|
|
44
|
+
else
|
|
45
|
+
@patterns << normalize_pattern(line)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
# If we can't parse .gitignore, just continue with empty patterns
|
|
50
|
+
warn "Warning: Failed to parse .gitignore: #{e.message}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def normalize_pattern(pattern)
|
|
54
|
+
pattern = pattern.strip
|
|
55
|
+
|
|
56
|
+
# Remove trailing whitespace
|
|
57
|
+
pattern = pattern.rstrip
|
|
58
|
+
|
|
59
|
+
# Store original for directory detection
|
|
60
|
+
is_directory = pattern.end_with?('/')
|
|
61
|
+
pattern = pattern.chomp('/')
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
pattern: pattern,
|
|
65
|
+
is_directory: is_directory,
|
|
66
|
+
is_absolute: pattern.start_with?('/'),
|
|
67
|
+
has_wildcard: pattern.include?('*') || pattern.include?('?'),
|
|
68
|
+
has_double_star: pattern.include?('**')
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def match_pattern?(path, pattern_info)
|
|
73
|
+
pattern = pattern_info[:pattern]
|
|
74
|
+
|
|
75
|
+
# Remove leading slash for absolute patterns
|
|
76
|
+
pattern = pattern[1..] if pattern_info[:is_absolute]
|
|
77
|
+
|
|
78
|
+
# Handle directory patterns
|
|
79
|
+
if pattern_info[:is_directory]
|
|
80
|
+
return false unless File.directory?(path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Handle different wildcard patterns
|
|
84
|
+
if pattern_info[:has_double_star]
|
|
85
|
+
# Convert ** to match any number of directories
|
|
86
|
+
regex_pattern = pattern
|
|
87
|
+
.gsub('**/', '(.*/)?') # **/ matches zero or more directories
|
|
88
|
+
.gsub('**', '.*') # ** at end matches anything
|
|
89
|
+
.gsub('*', '[^/]*') # * matches anything except /
|
|
90
|
+
.gsub('?', '[^/]') # ? matches single character except /
|
|
91
|
+
|
|
92
|
+
regex = Regexp.new("^#{regex_pattern}$")
|
|
93
|
+
return true if path.match?(regex)
|
|
94
|
+
return true if path.split('/').any? { |part| part.match?(regex) }
|
|
95
|
+
elsif pattern_info[:has_wildcard]
|
|
96
|
+
# Convert glob pattern to regex
|
|
97
|
+
regex_pattern = pattern
|
|
98
|
+
.gsub('*', '[^/]*')
|
|
99
|
+
.gsub('?', '[^/]')
|
|
100
|
+
|
|
101
|
+
regex = Regexp.new("^#{regex_pattern}$")
|
|
102
|
+
return true if path.match?(regex)
|
|
103
|
+
return true if File.basename(path).match?(regex)
|
|
104
|
+
else
|
|
105
|
+
# Exact match
|
|
106
|
+
return true if path == pattern
|
|
107
|
+
return true if path.start_with?("#{pattern}/")
|
|
108
|
+
return true if File.basename(path) == pattern
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|