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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c1d065edf333a21a538aff43effdcefbc1cb9fa9a3c816d2da3943418f502fb
4
- data.tar.gz: d891f1b03ff43bcd237eb510990b74145522404dd4fa3b2bc4b3741f3e495a32
3
+ metadata.gz: f86dc057b8fd69db07c40a5633ffe1350cf0ddb8509b6e26705464d9e127daa4
4
+ data.tar.gz: 58d3e6b89e129d6c7ef7dbc57ff57e95ee8d6acd96186491daa8670b6031c3b2
5
5
  SHA512:
6
- metadata.gz: 5190473333b6c0259ee7ec76bf3127986b03fcbbaeba8face1e0f5bd5ebc60d40b9937df7ef129737eb6364789a214a515970728cfd71b80770cc671e5440004
7
- data.tar.gz: 0a7858fe351e34810bca0b28ab91b3974041758608fe8c446e6f0c555402c990418bb6d3bc4ac071fe61fe3d881a5c190f191258287541b57508a940c2643752
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 rubocop]
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 "readline"
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 Readline for better input handling (backspace, arrow keys, etc.)
863
- response = Readline.readline(" (Enter/y to approve, n to deny, or provide feedback): ", true)
862
+ # Use TTY::Prompt for better input handling
863
+ tty_prompt = TTY::Prompt.new(interrupt: :exit)
864
864
 
865
- if response.nil? # Handle EOF/pipe input
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 = response.chomp
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
- # Empty response (just Enter) or "y"/"yes" = approved
873
- if response.empty? || response_lower == "y" || response_lower == "yes"
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/prompt"
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
- current_message = prompt.read_input(prefix: "You:")
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::Prompt.new
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 Readline for better Unicode/CJK support
662
- message = Readline.readline("You: ", true)
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.downcase.strip)
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
@@ -49,6 +49,7 @@ module Clacky
49
49
  print "\e[u" # Restore cursor position (to after [..] symbol)
50
50
  print "\e[K" # Clear to end of line from cursor
51
51
  print text
52
+ print " "
52
53
  $stdout.flush
53
54
  end
54
55
  end