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.
- checksums.yaml +4 -4
- data/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|