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
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -1,34 +1,407 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
3
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
|
+
# Clacky environment variable layer
|
|
50
|
+
# Provides configuration from CLACKY_XXX environment variables
|
|
51
|
+
module ClackyEnv
|
|
52
|
+
# Environment variable names for default model
|
|
53
|
+
ENV_API_KEY = "CLACKY_API_KEY"
|
|
54
|
+
ENV_BASE_URL = "CLACKY_BASE_URL"
|
|
55
|
+
ENV_MODEL = "CLACKY_MODEL"
|
|
56
|
+
ENV_ANTHROPIC_FORMAT = "CLACKY_ANTHROPIC_FORMAT"
|
|
57
|
+
|
|
58
|
+
# Environment variable names for lite model
|
|
59
|
+
ENV_LITE_API_KEY = "CLACKY_LITE_API_KEY"
|
|
60
|
+
ENV_LITE_BASE_URL = "CLACKY_LITE_BASE_URL"
|
|
61
|
+
ENV_LITE_MODEL = "CLACKY_LITE_MODEL"
|
|
62
|
+
ENV_LITE_ANTHROPIC_FORMAT = "CLACKY_LITE_ANTHROPIC_FORMAT"
|
|
63
|
+
|
|
64
|
+
# Default model name (only for model, not base_url)
|
|
65
|
+
DEFAULT_MODEL = "claude-sonnet-4-5"
|
|
66
|
+
|
|
67
|
+
class << self
|
|
68
|
+
# Check if default model is configured via environment variables
|
|
69
|
+
def default_configured?
|
|
70
|
+
!default_api_key.nil? && !default_api_key.empty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check if lite model is configured via environment variables
|
|
74
|
+
def lite_configured?
|
|
75
|
+
!lite_api_key.nil? && !lite_api_key.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get default model API key
|
|
79
|
+
def default_api_key
|
|
80
|
+
ENV[ENV_API_KEY] if ENV[ENV_API_KEY] && !ENV[ENV_API_KEY].empty?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get default model base URL (no default, must be explicitly set)
|
|
84
|
+
def default_base_url
|
|
85
|
+
ENV[ENV_BASE_URL] if ENV[ENV_BASE_URL] && !ENV[ENV_BASE_URL].empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Get default model name
|
|
89
|
+
def default_model
|
|
90
|
+
ENV[ENV_MODEL] && !ENV[ENV_MODEL].empty? ? ENV[ENV_MODEL] : DEFAULT_MODEL
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get default model anthropic_format flag
|
|
94
|
+
def default_anthropic_format
|
|
95
|
+
return true if ENV[ENV_ANTHROPIC_FORMAT].nil? || ENV[ENV_ANTHROPIC_FORMAT].empty?
|
|
96
|
+
ENV[ENV_ANTHROPIC_FORMAT].downcase == "true"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get default model configuration as a hash
|
|
100
|
+
def default_model_config
|
|
101
|
+
{
|
|
102
|
+
"type" => "default",
|
|
103
|
+
"api_key" => default_api_key,
|
|
104
|
+
"base_url" => default_base_url,
|
|
105
|
+
"model" => default_model,
|
|
106
|
+
"anthropic_format" => default_anthropic_format
|
|
107
|
+
}.compact
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get lite model API key
|
|
111
|
+
def lite_api_key
|
|
112
|
+
ENV[ENV_LITE_API_KEY] if ENV[ENV_LITE_API_KEY] && !ENV[ENV_LITE_API_KEY].empty?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Get lite model base URL (no default, must be explicitly set)
|
|
116
|
+
def lite_base_url
|
|
117
|
+
ENV[ENV_LITE_BASE_URL] if ENV[ENV_LITE_BASE_URL] && !ENV[ENV_LITE_BASE_URL].empty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get lite model name
|
|
121
|
+
def lite_model
|
|
122
|
+
ENV[ENV_LITE_MODEL] && !ENV[ENV_LITE_MODEL].empty? ? ENV[ENV_LITE_MODEL] : "claude-haiku-4"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get lite model anthropic_format flag
|
|
126
|
+
def lite_anthropic_format
|
|
127
|
+
return true if ENV[ENV_LITE_ANTHROPIC_FORMAT].nil? || ENV[ENV_LITE_ANTHROPIC_FORMAT].empty?
|
|
128
|
+
ENV[ENV_LITE_ANTHROPIC_FORMAT].downcase == "true"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Get lite model configuration as a hash
|
|
132
|
+
def lite_model_config
|
|
133
|
+
{
|
|
134
|
+
"type" => "lite",
|
|
135
|
+
"api_key" => lite_api_key,
|
|
136
|
+
"base_url" => lite_base_url,
|
|
137
|
+
"model" => lite_model,
|
|
138
|
+
"anthropic_format" => lite_anthropic_format
|
|
139
|
+
}.compact
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
4
144
|
class AgentConfig
|
|
5
|
-
|
|
6
|
-
|
|
145
|
+
CONFIG_DIR = File.join(Dir.home, ".clacky")
|
|
146
|
+
CONFIG_FILE = File.join(CONFIG_DIR, "config.yml")
|
|
147
|
+
|
|
148
|
+
# Default model for ClaudeCode environment
|
|
149
|
+
CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-5"
|
|
7
150
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
151
|
+
PERMISSION_MODES = [:auto_approve, :confirm_safes, :plan_only].freeze
|
|
152
|
+
|
|
153
|
+
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
154
|
+
:enable_compression, :enable_prompt_caching,
|
|
155
|
+
:models, :current_model_index
|
|
11
156
|
|
|
12
157
|
def initialize(options = {})
|
|
13
|
-
@model = options[:model] || "gpt-3.5-turbo"
|
|
14
158
|
@permission_mode = validate_permission_mode(options[:permission_mode])
|
|
15
159
|
@max_tokens = options[:max_tokens] || 8192
|
|
16
160
|
@verbose = options[:verbose] || false
|
|
17
161
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
18
|
-
@keep_recent_messages = options[:keep_recent_messages] || 20
|
|
19
162
|
# Enable prompt caching by default for cost savings
|
|
20
163
|
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
164
|
+
|
|
165
|
+
# Models configuration
|
|
166
|
+
@models = options[:models] || []
|
|
167
|
+
@current_model_index = options[:current_model_index] || 0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Load configuration from file
|
|
171
|
+
def self.load(config_file = CONFIG_FILE)
|
|
172
|
+
# Load from config file first
|
|
173
|
+
if File.exist?(config_file)
|
|
174
|
+
data = YAML.load_file(config_file)
|
|
175
|
+
else
|
|
176
|
+
data = nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Parse models from config
|
|
180
|
+
models = parse_models(data)
|
|
181
|
+
|
|
182
|
+
# Priority: config file > CLACKY_XXX env vars > ClaudeCode env vars
|
|
183
|
+
if models.empty?
|
|
184
|
+
# Try CLACKY_XXX environment variables first
|
|
185
|
+
if ClackyEnv.default_configured?
|
|
186
|
+
models << ClackyEnv.default_model_config
|
|
187
|
+
# Fallback to ClaudeCode environment variables
|
|
188
|
+
elsif ClaudeCodeEnv.configured?
|
|
189
|
+
models << {
|
|
190
|
+
"type" => "default",
|
|
191
|
+
"api_key" => ClaudeCodeEnv.api_key,
|
|
192
|
+
"base_url" => ClaudeCodeEnv.base_url,
|
|
193
|
+
"model" => CLAUDE_DEFAULT_MODEL,
|
|
194
|
+
"anthropic_format" => true
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Add CLACKY_LITE_XXX if configured (only when loading from env)
|
|
199
|
+
if ClackyEnv.lite_configured?
|
|
200
|
+
models << ClackyEnv.lite_model_config
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
# Config file exists, but check if we need to add env-based models
|
|
204
|
+
# Only add if no model with that type exists
|
|
205
|
+
has_default = models.any? { |m| m["type"] == "default" }
|
|
206
|
+
has_lite = models.any? { |m| m["type"] == "lite" }
|
|
207
|
+
|
|
208
|
+
# Add CLACKY default if not in config and env is set
|
|
209
|
+
if !has_default && ClackyEnv.default_configured?
|
|
210
|
+
models << ClackyEnv.default_model_config
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Add CLACKY lite if not in config and env is set
|
|
214
|
+
if !has_lite && ClackyEnv.lite_configured?
|
|
215
|
+
models << ClackyEnv.lite_model_config
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Ensure at least one model has type: default
|
|
219
|
+
# If no model has type: default, assign it to the first model
|
|
220
|
+
unless models.any? { |m| m["type"] == "default" }
|
|
221
|
+
models.first["type"] = "default" if models.any?
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
new(models: models)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Save configuration to file
|
|
229
|
+
def save(config_file = CONFIG_FILE)
|
|
230
|
+
config_dir = File.dirname(config_file)
|
|
231
|
+
FileUtils.mkdir_p(config_dir)
|
|
232
|
+
File.write(config_file, to_yaml)
|
|
233
|
+
FileUtils.chmod(0o600, config_file)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Convert to YAML format (top-level array)
|
|
237
|
+
def to_yaml
|
|
238
|
+
YAML.dump(@models)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Check if any model is configured
|
|
242
|
+
def models_configured?
|
|
243
|
+
!@models.empty? && !current_model.nil?
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Get current model configuration
|
|
247
|
+
def current_model
|
|
248
|
+
return nil if @models.empty?
|
|
249
|
+
@models[@current_model_index]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Get model by index
|
|
253
|
+
def get_model(index)
|
|
254
|
+
@models[index]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Switch to model by index
|
|
258
|
+
# Updates the type: default to the selected model
|
|
259
|
+
# Returns true if switched, false if index out of range
|
|
260
|
+
def switch_model(index)
|
|
261
|
+
return false if index < 0 || index >= @models.length
|
|
262
|
+
|
|
263
|
+
# Remove type: default from all models
|
|
264
|
+
@models.each { |m| m.delete("type") if m["type"] == "default" }
|
|
265
|
+
|
|
266
|
+
# Set type: default on the selected model
|
|
267
|
+
@models[index]["type"] = "default"
|
|
268
|
+
|
|
269
|
+
# Update current_model_index for backward compatibility
|
|
270
|
+
@current_model_index = index
|
|
271
|
+
|
|
272
|
+
true
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# List all model names
|
|
276
|
+
def model_names
|
|
277
|
+
@models.map { |m| m["model"] }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Get API key for current model
|
|
281
|
+
def api_key
|
|
282
|
+
current_model&.dig("api_key")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Set API key for current model
|
|
286
|
+
def api_key=(value)
|
|
287
|
+
return unless current_model
|
|
288
|
+
current_model["api_key"] = value
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Get base URL for current model
|
|
292
|
+
def base_url
|
|
293
|
+
current_model&.dig("base_url")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Set base URL for current model
|
|
297
|
+
def base_url=(value)
|
|
298
|
+
return unless current_model
|
|
299
|
+
current_model["base_url"] = value
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Get model name for current model
|
|
303
|
+
def model_name
|
|
304
|
+
current_model&.dig("model")
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Set model name for current model
|
|
308
|
+
def model_name=(value)
|
|
309
|
+
return unless current_model
|
|
310
|
+
current_model["model"] = value
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Check if should use Anthropic format for current model
|
|
314
|
+
def anthropic_format?
|
|
315
|
+
current_model&.dig("anthropic_format") || false
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Add a new model configuration
|
|
319
|
+
def add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil)
|
|
320
|
+
@models << {
|
|
321
|
+
"api_key" => api_key,
|
|
322
|
+
"base_url" => base_url,
|
|
323
|
+
"model" => model,
|
|
324
|
+
"anthropic_format" => anthropic_format,
|
|
325
|
+
"type" => type
|
|
326
|
+
}.compact
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Find model by type (default or lite)
|
|
330
|
+
# Returns the model hash or nil if not found
|
|
331
|
+
def find_model_by_type(type)
|
|
332
|
+
@models.find { |m| m["type"] == type }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Get the default model (type: default)
|
|
336
|
+
# Falls back to current_model for backward compatibility
|
|
337
|
+
def default_model
|
|
338
|
+
find_model_by_type("default") || current_model
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Get the lite model (type: lite)
|
|
342
|
+
# Returns nil if no lite model configured
|
|
343
|
+
def lite_model
|
|
344
|
+
find_model_by_type("lite")
|
|
21
345
|
end
|
|
22
346
|
|
|
347
|
+
# Get current model configuration
|
|
348
|
+
# Looks for type: default first, falls back to current_model_index
|
|
349
|
+
def current_model
|
|
350
|
+
return nil if @models.empty?
|
|
351
|
+
default_model = find_model_by_type("default")
|
|
352
|
+
return default_model if default_model
|
|
353
|
+
|
|
354
|
+
# Fallback to index-based for backward compatibility
|
|
355
|
+
@models[@current_model_index]
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Set a model's type (default or lite)
|
|
359
|
+
# Ensures only one model has each type
|
|
360
|
+
# @param index [Integer] the model index
|
|
361
|
+
# @param type [String, nil] "default", "lite", or nil to remove type
|
|
362
|
+
# Returns true if successful
|
|
363
|
+
def set_model_type(index, type)
|
|
364
|
+
return false if index < 0 || index >= @models.length
|
|
365
|
+
return false unless ["default", "lite", nil].include?(type)
|
|
23
366
|
|
|
367
|
+
if type
|
|
368
|
+
# Remove type from any other model that has it
|
|
369
|
+
@models.each do |m|
|
|
370
|
+
m.delete("type") if m["type"] == type
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Set type on target model
|
|
374
|
+
@models[index]["type"] = type
|
|
375
|
+
else
|
|
376
|
+
# Remove type from target model
|
|
377
|
+
@models[index].delete("type")
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
true
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Remove a model by index
|
|
384
|
+
# Returns true if removed, false if index out of range or it's the last model
|
|
385
|
+
def remove_model(index)
|
|
386
|
+
# Don't allow removing the last model
|
|
387
|
+
return false if @models.length <= 1
|
|
388
|
+
return false if index < 0 || index >= @models.length
|
|
389
|
+
|
|
390
|
+
@models.delete_at(index)
|
|
391
|
+
|
|
392
|
+
# Adjust current_model_index if necessary
|
|
393
|
+
if @current_model_index >= @models.length
|
|
394
|
+
@current_model_index = @models.length - 1
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
true
|
|
398
|
+
end
|
|
24
399
|
|
|
25
400
|
def is_plan_only?
|
|
26
401
|
@permission_mode == :plan_only
|
|
27
402
|
end
|
|
28
403
|
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def validate_permission_mode(mode)
|
|
404
|
+
private def validate_permission_mode(mode)
|
|
32
405
|
mode ||= :confirm_safes
|
|
33
406
|
mode = mode.to_sym
|
|
34
407
|
|
|
@@ -39,6 +412,70 @@ module Clacky
|
|
|
39
412
|
mode
|
|
40
413
|
end
|
|
41
414
|
|
|
415
|
+
# Parse models from config data
|
|
416
|
+
# Supports new top-level array format and old formats for backward compatibility
|
|
417
|
+
private_class_method def self.parse_models(data)
|
|
418
|
+
models = []
|
|
419
|
+
|
|
420
|
+
# Handle nil or empty data
|
|
421
|
+
return models if data.nil?
|
|
42
422
|
|
|
423
|
+
if data.is_a?(Array)
|
|
424
|
+
# New format: top-level array of model configurations
|
|
425
|
+
models = data.map do |m|
|
|
426
|
+
# Convert old name-based format to new model-based format if needed
|
|
427
|
+
if m["name"] && !m["model"]
|
|
428
|
+
m["model"] = m["name"]
|
|
429
|
+
m.delete("name")
|
|
430
|
+
end
|
|
431
|
+
m
|
|
432
|
+
end
|
|
433
|
+
elsif data.is_a?(Hash) && data["models"]
|
|
434
|
+
# Old format with "models:" key
|
|
435
|
+
if data["models"].is_a?(Array)
|
|
436
|
+
# Array under models key
|
|
437
|
+
models = data["models"].map do |m|
|
|
438
|
+
# Convert old name-based format to new model-based format
|
|
439
|
+
if m["name"] && !m["model"]
|
|
440
|
+
m["model"] = m["name"]
|
|
441
|
+
m.delete("name")
|
|
442
|
+
end
|
|
443
|
+
m
|
|
444
|
+
end
|
|
445
|
+
elsif data["models"].is_a?(Hash)
|
|
446
|
+
# Hash format with tier names as keys (very old format)
|
|
447
|
+
data["models"].each do |tier_name, config|
|
|
448
|
+
if config.is_a?(Hash)
|
|
449
|
+
model_config = {
|
|
450
|
+
"api_key" => config["api_key"],
|
|
451
|
+
"base_url" => config["base_url"],
|
|
452
|
+
"model" => config["model_name"] || config["model"] || tier_name,
|
|
453
|
+
"anthropic_format" => config["anthropic_format"] || false
|
|
454
|
+
}
|
|
455
|
+
models << model_config
|
|
456
|
+
elsif config.is_a?(String)
|
|
457
|
+
# Old-style tier with just model name
|
|
458
|
+
model_config = {
|
|
459
|
+
"api_key" => data["api_key"],
|
|
460
|
+
"base_url" => data["base_url"],
|
|
461
|
+
"model" => config,
|
|
462
|
+
"anthropic_format" => data["anthropic_format"] || false
|
|
463
|
+
}
|
|
464
|
+
models << model_config
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
elsif data.is_a?(Hash) && data["api_key"]
|
|
469
|
+
# Very old format: single model with global config
|
|
470
|
+
models << {
|
|
471
|
+
"api_key" => data["api_key"],
|
|
472
|
+
"base_url" => data["base_url"],
|
|
473
|
+
"model" => data["model"] || CLAUDE_DEFAULT_MODEL,
|
|
474
|
+
"anthropic_format" => data["anthropic_format"] || false
|
|
475
|
+
}
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
models
|
|
479
|
+
end
|
|
43
480
|
end
|
|
44
481
|
end
|