openclacky 0.7.0 → 0.7.2

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +29 -4
  3. data/.clackyrules +3 -1
  4. data/CHANGELOG.md +103 -2
  5. data/README.md +70 -161
  6. data/bin/clarky +11 -0
  7. data/docs/HOW-TO-USE-CN.md +96 -0
  8. data/docs/HOW-TO-USE.md +94 -0
  9. data/docs/config.example.yml +27 -0
  10. data/docs/deploy_subagent_design.md +540 -0
  11. data/docs/time_machine_design.md +247 -0
  12. data/docs/why-openclacky.md +0 -1
  13. data/lib/clacky/agent/cost_tracker.rb +180 -0
  14. data/lib/clacky/agent/llm_caller.rb +54 -0
  15. data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
  16. data/lib/clacky/agent/message_compressor_helper.rb +534 -0
  17. data/lib/clacky/agent/session_serializer.rb +152 -0
  18. data/lib/clacky/agent/skill_manager.rb +138 -0
  19. data/lib/clacky/agent/system_prompt_builder.rb +96 -0
  20. data/lib/clacky/agent/time_machine.rb +199 -0
  21. data/lib/clacky/agent/tool_executor.rb +434 -0
  22. data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
  23. data/lib/clacky/agent.rb +260 -1370
  24. data/lib/clacky/agent_config.rb +447 -10
  25. data/lib/clacky/cli.rb +275 -98
  26. data/lib/clacky/client.rb +12 -2
  27. data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
  28. data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
  29. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
  30. data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
  31. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
  32. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
  33. data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
  34. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
  35. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
  36. data/lib/clacky/default_skills/new/SKILL.md +2 -2
  37. data/lib/clacky/json_ui_controller.rb +195 -0
  38. data/lib/clacky/providers.rb +107 -0
  39. data/lib/clacky/skill.rb +48 -7
  40. data/lib/clacky/skill_loader.rb +7 -0
  41. data/lib/clacky/tools/edit.rb +105 -48
  42. data/lib/clacky/tools/file_reader.rb +44 -73
  43. data/lib/clacky/tools/invoke_skill.rb +89 -0
  44. data/lib/clacky/tools/list_tasks.rb +54 -0
  45. data/lib/clacky/tools/redo_task.rb +41 -0
  46. data/lib/clacky/tools/safe_shell.rb +1 -1
  47. data/lib/clacky/tools/shell.rb +74 -62
  48. data/lib/clacky/tools/trash_manager.rb +1 -1
  49. data/lib/clacky/tools/undo_task.rb +32 -0
  50. data/lib/clacky/tools/web_fetch.rb +2 -1
  51. data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
  52. data/lib/clacky/ui2/components/inline_input.rb +23 -2
  53. data/lib/clacky/ui2/components/input_area.rb +65 -21
  54. data/lib/clacky/ui2/components/modal_component.rb +199 -62
  55. data/lib/clacky/ui2/layout_manager.rb +75 -25
  56. data/lib/clacky/ui2/line_editor.rb +23 -2
  57. data/lib/clacky/ui2/markdown_renderer.rb +31 -10
  58. data/lib/clacky/ui2/screen_buffer.rb +2 -0
  59. data/lib/clacky/ui2/ui_controller.rb +316 -37
  60. data/lib/clacky/ui2.rb +2 -0
  61. data/lib/clacky/ui_interface.rb +50 -0
  62. data/lib/clacky/utils/arguments_parser.rb +31 -3
  63. data/lib/clacky/utils/file_processor.rb +13 -18
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky.rb +19 -9
  66. data/scripts/install.sh +274 -97
  67. data/scripts/uninstall.sh +12 -12
  68. metadata +40 -13
  69. data/.clacky/skills/test-skill/SKILL.md +0 -15
  70. data/lib/clacky/compression/base.rb +0 -231
  71. data/lib/clacky/compression/standard.rb +0 -339
  72. data/lib/clacky/config.rb +0 -117
  73. /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
  74. /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
  75. /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
  76. /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
  77. /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
  78. /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
@@ -1,339 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base"
4
-
5
- module Clacky
6
- module Compression
7
- # Standard compression strategy - preserves the original compression logic
8
- #
9
- # This strategy:
10
- # 1. Keeps the system message (first message)
11
- # 2. Keeps recent N messages (maintaining tool call/result pairs)
12
- # 3. Compresses all middle messages into a hierarchical summary
13
- #
14
- # Configuration options:
15
- # - threshold: Token count to trigger compression (default: 80_000)
16
- # - message_threshold: Message count to trigger compression (default: 100)
17
- # - target_tokens: Target size after compression (default: 70_000)
18
- # - max_recent: Maximum recent messages to keep (default: 30)
19
- #
20
- class Standard < Base
21
- STRATEGY_NAME = :standard
22
-
23
- # Compression thresholds
24
- THRESHOLD = 80_000
25
- MESSAGE_COUNT_THRESHOLD = 100
26
- TARGET_COMPRESSED_TOKENS = 70_000
27
- MAX_RECENT_MESSAGES = 30
28
-
29
- def initialize(options = {})
30
- super(options)
31
- @recent_messages = []
32
- @summary = nil
33
- end
34
-
35
- def strategy_name
36
- STRATEGY_NAME
37
- end
38
-
39
- # Main compression method
40
- # @param messages [Array<Hash>] All conversation messages
41
- # @param options [Hash] Additional options
42
- # @return [Array<Hash>] Compressed messages
43
- def compress(messages, options = {})
44
- increment_count
45
-
46
- # Calculate total tokens and message count
47
- token_counts = total_message_tokens(messages)
48
- total_tokens = token_counts[:total]
49
- message_count = messages.length
50
-
51
- # Check if compression is needed
52
- token_threshold_exceeded = total_tokens >= threshold(options)
53
- message_count_exceeded = message_count >= message_count_threshold(options)
54
-
55
- # Return original if no threshold exceeded
56
- unless token_threshold_exceeded || message_count_exceeded
57
- return messages.dup
58
- end
59
-
60
- # Calculate how much we need to reduce
61
- reduction_needed = total_tokens - target_tokens(options)
62
-
63
- # Skip if reduction is minimal
64
- if token_threshold_exceeded && reduction_needed < (total_tokens * 0.1)
65
- return messages.dup
66
- end
67
-
68
- # Calculate target recent count
69
- target_recent_count = calculate_target_recent_count(reduction_needed)
70
-
71
- # Find system message
72
- system_msg = messages.find { |m| m[:role] == "system" }
73
-
74
- # Get recent messages with tool pairs
75
- recent = get_recent_messages_with_tool_pairs(messages, target_recent_count)
76
- recent = [] if recent.nil?
77
-
78
- # Get messages to compress
79
- messages_to_compress = messages.reject { |m| m[:role] == "system" || recent.include?(m) }
80
-
81
- # Return if nothing to compress
82
- if messages_to_compress.empty?
83
- return [system_msg, *recent].compact
84
- end
85
-
86
- # Generate hierarchical summary
87
- summary = generate_hierarchical_summary(messages_to_compress)
88
-
89
- # Rebuild messages
90
- @recent_messages = recent
91
- @summary = summary
92
-
93
- [system_msg, summary, *recent].compact
94
- end
95
-
96
- # Compression statistics
97
- def stats
98
- super.merge(
99
- recent_messages_count: @recent_messages&.length || 0,
100
- has_summary: !@summary.nil?
101
- )
102
- end
103
-
104
- private
105
-
106
- def threshold(options)
107
- options[:threshold] || THRESHOLD
108
- end
109
-
110
- def message_count_threshold(options)
111
- options[:message_count_threshold] || MESSAGE_COUNT_THRESHOLD
112
- end
113
-
114
- def target_tokens(options)
115
- options[:target_tokens] || TARGET_COMPRESSED_TOKENS
116
- end
117
-
118
- def max_recent_messages(options)
119
- options[:max_recent] || MAX_RECENT_MESSAGES
120
- end
121
-
122
- def calculate_target_recent_count(reduction_needed)
123
- tokens_per_message = 500
124
- recent_budget = (target_tokens(@options) * 0.2).to_i
125
- target_messages = (recent_budget / tokens_per_message).to_i
126
- [[target_messages, 20].max, max_recent_messages(@options)].min
127
- end
128
-
129
- # Ensure tool calls and their results are kept together
130
- def get_recent_messages_with_tool_pairs(messages, count)
131
- return [] if messages.nil? || messages.empty?
132
-
133
- included = Set.new
134
- collected = 0
135
- i = messages.size - 1
136
-
137
- while i >= 0 && collected < count
138
- msg = messages[i]
139
- next if included.include?(i)
140
-
141
- included.add(i)
142
- collected += 1
143
-
144
- # If assistant with tool_calls, include all corresponding results
145
- if msg[:role] == "assistant" && msg[:tool_calls]
146
- tool_call_ids = msg[:tool_calls].map { |tc| tc[:id] }
147
-
148
- j = i + 1
149
- while j < messages.size
150
- next_msg = messages[j]
151
-
152
- if next_msg[:role] == "tool" && tool_call_ids.include?(next_msg[:tool_call_id])
153
- included.add(j)
154
- elsif next_msg[:role] != "tool"
155
- break
156
- end
157
-
158
- j += 1
159
- end
160
- end
161
-
162
- # If tool result, ensure its assistant message is included
163
- if msg[:role] == "tool"
164
- j = i - 1
165
- while j >= 0
166
- prev_msg = messages[j]
167
-
168
- if prev_msg[:role] == "assistant" && prev_msg[:tool_calls]
169
- has_match = prev_msg[:tool_calls].any? { |tc| tc[:id] == msg[:tool_call_id] }
170
-
171
- if has_match
172
- unless included.include?(j)
173
- included.add(j)
174
- collected += 1
175
- end
176
-
177
- # Include all tool results for this assistant
178
- tool_ids = prev_msg[:tool_calls].map { |tc| tc[:id] }
179
- k = j + 1
180
- while k < messages.size
181
- if messages[k][:role] == "tool" && tool_ids.include?(messages[k][:tool_call_id])
182
- included.add(k)
183
- elsif messages[k][:role] != "tool"
184
- break
185
- end
186
- k += 1
187
- end
188
-
189
- break
190
- end
191
- end
192
-
193
- j -= 1
194
- end
195
- end
196
-
197
- i -= 1
198
- end
199
-
200
- included.to_a.sort.map { |idx| messages[idx] }
201
- end
202
-
203
- # Generate summary with progressive levels
204
- def generate_hierarchical_summary(messages)
205
- level = @compression_count
206
-
207
- # Adjust level to max 4
208
- level = [level, 4].min
209
-
210
- data = extract_key_information(messages)
211
- summary_text = build_summary_text(data, level)
212
-
213
- {
214
- role: "user",
215
- content: "[SYSTEM][COMPRESSION LEVEL #{level}] #{summary_text}",
216
- system_injected: true,
217
- compression_level: level,
218
- compression_count: @compression_count,
219
- compression_strategy: :standard
220
- }
221
- end
222
-
223
- # Extract key info from messages
224
- def extract_key_information(messages)
225
- return empty_extraction_data if messages.nil?
226
-
227
- {
228
- user_msgs: messages.count { |m| m[:role] == "user" },
229
- assistant_msgs: messages.count { |m| m[:role] == "assistant" },
230
- tool_msgs: messages.count { |m| m[:role] == "tool" },
231
- tools_used: extract_tool_names(messages),
232
- files_created: extract_files(messages, :created),
233
- files_modified: extract_files(messages, :modified),
234
- decisions: extract_decisions(messages).first(5),
235
- completed_tasks: extract_completed_tasks(messages),
236
- in_progress: find_in_progress(messages),
237
- errors: extract_errors(messages)
238
- }
239
- end
240
-
241
- def extract_files(messages, action)
242
- messages
243
- .select { |m| m[:role] == "tool" }
244
- .map { |m| parse_file_action(m[:content], action) }
245
- .compact
246
- end
247
-
248
- def extract_completed_tasks(messages)
249
- messages
250
- .select { |m| m[:role] == "tool" && m[:content].is_a?(String) }
251
- .select { |m| m[:content].include?("completed") }
252
- .map { |m| parse_todo_result(m[:content]) }
253
- .compact
254
- end
255
-
256
- # Summary builders for each level
257
- def build_summary_text(data, level)
258
- case level
259
- when 1
260
- build_level1(data)
261
- when 2
262
- build_level2(data)
263
- when 3
264
- build_level3(data)
265
- else
266
- build_level4(data)
267
- end
268
- end
269
-
270
- def build_level1(data)
271
- parts = []
272
- parts << "Previous conversation summary (#{data[:user_msgs]} requests, #{data[:assistant_msgs]} responses, #{data[:tool_msgs]} tools):"
273
-
274
- if data[:files_created].any?
275
- parts << "Created: #{data[:files_created].map { |f| File.basename(f) }.join(', ')}"
276
- end
277
-
278
- if data[:files_modified].any?
279
- parts << "Modified: #{data[:files_modified].map { |f| File.basename(f) }.join(', ')}"
280
- end
281
-
282
- if data[:completed_tasks].any?
283
- parts << "Completed: #{data[:completed_tasks].first(3).join(', ')}"
284
- end
285
-
286
- if data[:in_progress]
287
- parts << "In Progress: #{data[:in_progress]}"
288
- end
289
-
290
- if data[:decisions].any?
291
- parts << "Decisions: #{data[:decisions].map { |d| d.gsub("\n", " ").strip }.join('; ')}"
292
- end
293
-
294
- if data[:tools_used].any?
295
- parts << "Tools: #{data[:tools_used].join(', ')}"
296
- end
297
-
298
- parts << "Continuing with recent conversation..."
299
- parts.join("\n")
300
- end
301
-
302
- def build_level2(data)
303
- parts = ["Conversation summary:"]
304
-
305
- all_files = (data[:files_created] + data[:files_modified]).uniq
306
- if all_files.any?
307
- parts << "Files: #{all_files.first(5).map { |f| File.basename(f) }.join(', ')}"
308
- end
309
-
310
- accomplishments = []
311
- accomplishments << "#{data[:completed_tasks].size} tasks completed" if data[:completed_tasks].any?
312
- accomplishments << "#{data[:tool_msgs]} tools executed" if data[:tool_msgs] > 0
313
-
314
- parts << accomplishments.join(', ') if accomplishments.any?
315
- parts << "Recent context follows..."
316
- parts.join("\n")
317
- end
318
-
319
- def build_level3(data)
320
- parts = ["Project progress:"]
321
-
322
- all_files = (data[:files_created] + data[:files_modified]).uniq
323
- parts << "#{all_files.size} files modified, #{data[:completed_tasks].size} tasks done"
324
-
325
- if data[:in_progress]
326
- parts << "Currently: #{data[:in_progress]}"
327
- end
328
-
329
- parts << "See recent messages for details."
330
- parts.join("\n")
331
- end
332
-
333
- def build_level4(data)
334
- all_files = (data[:files_created] + data[:files_modified]).uniq
335
- "Progress: #{data[:completed_tasks].size} tasks, #{all_files.size} files. Recent: #{data[:tools_used].last(3).uniq.join(', ')}"
336
- end
337
- end
338
- end
339
- end
data/lib/clacky/config.rb DELETED
@@ -1,117 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
- require "fileutils"
5
-
6
- module Clacky
7
- # ClaudeCode environment variable compatibility layer
8
- # Provides configuration detection from ClaudeCode's environment variables
9
- module ClaudeCodeEnv
10
- # Environment variable names used by ClaudeCode
11
- ENV_API_KEY = "ANTHROPIC_API_KEY"
12
- ENV_AUTH_TOKEN = "ANTHROPIC_AUTH_TOKEN"
13
- ENV_BASE_URL = "ANTHROPIC_BASE_URL"
14
-
15
- # Default Anthropic API endpoint
16
- DEFAULT_BASE_URL = "https://api.anthropic.com"
17
-
18
- class << self
19
- # Check if any ClaudeCode authentication is configured
20
- def configured?
21
- !api_key.nil? && !api_key.empty?
22
- end
23
-
24
- # Get API key - prefer ANTHROPIC_API_KEY, fallback to ANTHROPIC_AUTH_TOKEN
25
- def api_key
26
- if ENV[ENV_API_KEY] && !ENV[ENV_API_KEY].empty?
27
- ENV[ENV_API_KEY]
28
- elsif ENV[ENV_AUTH_TOKEN] && !ENV[ENV_AUTH_TOKEN].empty?
29
- ENV[ENV_AUTH_TOKEN]
30
- end
31
- end
32
-
33
- # Get base URL from environment, or return default Anthropic API URL
34
- def base_url
35
- ENV[ENV_BASE_URL] && !ENV[ENV_BASE_URL].empty? ? ENV[ENV_BASE_URL] : DEFAULT_BASE_URL
36
- end
37
-
38
- # Get configuration as a hash (includes configured values)
39
- # Returns api_key and base_url (always available as there's a default)
40
- def to_h
41
- {
42
- "api_key" => api_key,
43
- "base_url" => base_url
44
- }.compact
45
- end
46
- end
47
- end
48
-
49
- class Config
50
- CONFIG_DIR = File.join(Dir.home, ".clacky")
51
- CONFIG_FILE = File.join(CONFIG_DIR, "config.yml")
52
-
53
- # Default model for ClaudeCode environment
54
- CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-5"
55
-
56
- attr_accessor :api_key, :model, :base_url, :config_source
57
-
58
- def initialize(data = {})
59
- @api_key = data["api_key"]
60
- @model = data["model"]
61
- @base_url = data["base_url"]
62
- @config_source = data["_config_source"]
63
- end
64
-
65
- def self.load(config_file = CONFIG_FILE)
66
- # Load from config file first
67
- if File.exist?(config_file)
68
- data = YAML.load_file(config_file) || {}
69
- config_source = "file"
70
- else
71
- data = {}
72
- config_source = nil
73
- end
74
-
75
- # # If api_key not found in config file, check ClaudeCode environment variables
76
- # if data["api_key"].nil? || data["api_key"].empty?
77
- # if ClaudeCodeEnv.configured?
78
- # data["api_key"] = ClaudeCodeEnv.api_key
79
- # data["base_url"] = ClaudeCodeEnv.base_url if data["base_url"].nil? || data["base_url"].empty?
80
- # # Use Claude default model if not specified in config file
81
- # data["model"] = CLAUDE_DEFAULT_MODEL if data["model"].nil? || data["model"].empty?
82
- # config_source = "claude_code"
83
- # elsif config_source.nil?
84
- # config_source = "default"
85
- # end
86
- # elsif config_source.nil?
87
- # # Config file existed but didn't have api_key
88
- # config_source = "default"
89
- # end
90
-
91
- data["_config_source"] = config_source
92
- new(data)
93
- end
94
-
95
- def save(config_file = CONFIG_FILE)
96
- config_dir = File.dirname(config_file)
97
- FileUtils.mkdir_p(config_dir)
98
- File.write(config_file, to_yaml)
99
- FileUtils.chmod(0o600, config_file)
100
- end
101
-
102
- def to_yaml
103
- YAML.dump({
104
- "api_key" => @api_key,
105
- "model" => @model,
106
- "base_url" => @base_url
107
- })
108
- end
109
-
110
- # Determine if API calls should use Anthropic format (v1/messages)
111
- # Returns true only when config was loaded from ANTHROPIC_* environment variables
112
- # Config file users are expected to use OpenAI-compatible providers (OpenRouter, etc.)
113
- def anthropic_format?
114
- @config_source == "claude_code"
115
- end
116
- end
117
- end