openclacky 1.0.0.beta.2 → 1.0.0.beta.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: a6a51d00de51f04f142d6be7e3e726561384752ca02f44a6971eeaad9c2cdb28
4
- data.tar.gz: 185e635750793082206332377649095313b62297a2bd2f9f0b825a523a499afb
3
+ metadata.gz: 6793e56434ea9d620acea58d78487dcee09a204ec1fd692dd6057397334b9fad
4
+ data.tar.gz: 44e54de50aca54413f3668ee4f01b7c16673005dc61752cf2829d47c921a4734
5
5
  SHA512:
6
- metadata.gz: 66e14242f2d4b0e049fd45283c77c71919614d5f2ef8558b8635318f443afd41ab3ebf941a3b10a22c987805569082f699683af5eae23f5ef54e6dbd993e4931
7
- data.tar.gz: 4734d321c296e168f505185e67bddf8967687fd4889871a9a148f8bc003a7bb866774eb4f8c15ab4aba8b27384ae28e1c436477b3dd65e898c90743b393dbfb9
6
+ metadata.gz: 53696e16fd895822b06b613edcbd7ca154e7f7c50f813ef203ace7626209c725a5a1560d7f19a4cc71685b480761c64598162fac96f1d9a7b4bc61aec28d4d35
7
+ data.tar.gz: 7d5554d91399d9a07a4396c6400189f8a404691aa87fcc3f5c31a828d4044840741895003747932de2462162aef670996d30a487352bf6b2656ff08fa2e2ceb9
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.0.beta.4] - 2026-04-28
11
+
12
+ ### Fixed
13
+ - **首次配置 API Key 时报 JSON 解析错误。** 初始化向导(Onboard)保存 API 配置时调用了已废弃的旧接口 `POST /api/config`,该接口在 beta.2 重构后已不存在,服务器返回 404 导致前端报 `Unexpected token 'N', "Not Found" is not valid JSON`。现已修复,改为调用正确的新接口 `POST /api/config/models`。
14
+
15
+ ## [1.0.0.beta.3] - 2026-04-28
16
+
17
+ ### Added
18
+ - **Gemini 2.5 Pro support.** The new `gemini2.5-pro` model is now available as a selectable option, giving you access to Google's latest flagship model.
19
+ - **File attachments now support Markdown, plain text, and `.tar.gz` archives.** When you attach `.md`, `.txt`, or `.tar.gz` files to a session, the agent can read and reason over their contents directly.
20
+ - **Image type auto-detection.** Image files are now correctly identified by their binary content (magic bytes), not just their file extension — preventing misclassified images from causing upload or vision errors.
21
+
22
+ ### Improved
23
+ - **Settings page fully revamped.** The Web UI Settings panel now saves configuration correctly and exposes a richer set of options for managing providers, models, and API keys.
24
+ - **Skills no longer have a 50-item cap.** The skill loader previously limited the results list to 50 entries; that cap has been removed so all available skills show up.
25
+ - **Cost tracking no longer requires a hard-coded price list.** Model pricing is now resolved dynamically, so new models show real cost figures instead of falling back to a default.
26
+
27
+ ### Fixed
28
+ - **Terminal tool no longer crashes on non-UTF-8 output.** Commands that emit binary or non-UTF-8 bytes (e.g. compiled output, legacy scripts) no longer raise an encoding error in the terminal tool.
29
+
10
30
  ## [1.0.0.beta.2] - 2026-04-27
11
31
 
12
32
  ### Added
@@ -24,15 +24,22 @@ module Clacky
24
24
  cost = result[:cost]
25
25
  pricing_source = result[:source]
26
26
 
27
- @total_cost += cost
28
- iteration_cost = cost
29
- # Map pricing source to cost source: :price or :default
30
- @cost_source = pricing_source
31
- @task_cost_source = pricing_source
27
+ # Only accumulate cost when the model has known pricing.
28
+ # Unknown models return nil — display N/A, don't add to total.
29
+ if cost
30
+ @total_cost += cost
31
+ iteration_cost = cost
32
+ @cost_source = pricing_source
33
+ @task_cost_source = pricing_source
34
+ end
32
35
 
33
36
  if @config.verbose
34
- source_label = pricing_source == :price ? "model pricing" : "default pricing"
35
- @ui&.log("Calculated cost for #{@config.model_name} using #{source_label}: $#{cost.round(6)}", level: :debug)
37
+ if cost
38
+ source_label = pricing_source == :price ? "model pricing" : "default pricing"
39
+ @ui&.log("Calculated cost for #{@config.model_name} using #{source_label}: $#{cost.round(6)}", level: :debug)
40
+ else
41
+ @ui&.log("No pricing data available for #{@config.model_name} — cost is unknown", level: :debug)
42
+ end
36
43
  @ui&.log("Usage breakdown: prompt=#{usage[:prompt_tokens]}, completion=#{usage[:completion_tokens]}, cache_write=#{usage[:cache_creation_input_tokens] || 0}, cache_read=#{usage[:cache_read_input_tokens] || 0}", level: :debug)
37
44
  end
38
45
  end
@@ -18,6 +18,10 @@ module Clacky
18
18
  @working_dir = session_data[:working_dir]
19
19
  @created_at = session_data[:created_at]
20
20
  @total_tasks = session_data.dig(:stats, :total_tasks) || 0
21
+ # Restore cost_source so frontend knows if cost is reliable
22
+ cost_src = session_data.dig(:stats, :cost_source)
23
+ @cost_source = (cost_src && cost_src.to_sym) || :estimated
24
+ @task_cost_source = :estimated
21
25
  # Restore source; fall back to :manual for sessions saved before this field existed
22
26
  @source = (session_data[:source] || "manual").to_sym
23
27
 
@@ -65,6 +69,7 @@ module Clacky
65
69
  total_tasks: @total_tasks,
66
70
  total_iterations: @iterations,
67
71
  total_cost_usd: @total_cost.round(4),
72
+ cost_source: @cost_source.to_s,
68
73
  duration_seconds: @start_time ? (Time.now - @start_time).round(2) : 0,
69
74
  last_status: status.to_s,
70
75
  cache_stats: @cache_stats,
data/lib/clacky/agent.rb CHANGED
@@ -477,6 +477,7 @@ module Clacky
477
477
  @ui&.show_complete(
478
478
  iterations: result[:iterations],
479
479
  cost: result[:total_cost_usd],
480
+ cost_source: result[:cost_source],
480
481
  duration: result[:duration_seconds],
481
482
  cache_stats: result[:cache_stats],
482
483
  awaiting_user_feedback: awaiting_user_feedback
@@ -190,6 +190,10 @@ module Clacky
190
190
  auto_create_threshold: 12,
191
191
  reflection_mode: "llm_analysis"
192
192
  }
193
+ # Deep-symbolize keys — YAML-loaded hashes come with string keys,
194
+ # but the rest of the codebase accesses with symbols.
195
+ @skill_evolution = @skill_evolution.transform_keys(&:to_sym)
196
+ @skill_evolution.transform_values! { |v| v.is_a?(Hash) ? v.transform_keys(&:to_sym) : v }
193
197
 
194
198
  # Per-session virtual model overlay.
195
199
  # When set, #current_model returns a *merged* hash (the resolved @models
@@ -211,6 +215,13 @@ module Clacky
211
215
  data = nil
212
216
  end
213
217
 
218
+ # Extract settings from hash-format config (new format).
219
+ # Old flat-array configs have no settings section — all defaults.
220
+ loaded_settings = {}
221
+ if data.is_a?(Hash) && data["settings"].is_a?(Hash)
222
+ loaded_settings = data["settings"]
223
+ end
224
+
214
225
  # Parse models from config
215
226
  models = parse_models(data)
216
227
 
@@ -278,7 +289,21 @@ module Clacky
278
289
  default_index = models.find_index { |m| m["type"] == "default" } || 0
279
290
  default_id = models[default_index] && models[default_index]["id"]
280
291
 
281
- new(models: models, current_model_index: default_index, current_model_id: default_id)
292
+ # Build constructor args from loaded settings (new hash-format config)
293
+ # plus the parsed models. Only pass settings that have explicit values;
294
+ # omitted keys get their default from AgentConfig#initialize.
295
+ constructor_args = {
296
+ models: models,
297
+ current_model_index: default_index,
298
+ current_model_id: default_id
299
+ }
300
+ CONFIG_SETTINGS_KEYS.each do |key|
301
+ if loaded_settings.key?(key)
302
+ constructor_args[key.to_sym] = loaded_settings[key]
303
+ end
304
+ end
305
+
306
+ new(**constructor_args)
282
307
  end
283
308
 
284
309
  # Auto-injection of provider-preset lite models into @models has been
@@ -339,11 +364,27 @@ module Clacky
339
364
  # config.yml remains backward compatible with users on older versions.
340
365
  RUNTIME_ONLY_FIELDS = %w[id auto_injected].freeze
341
366
 
367
+ # Settings keys that are persisted to config.yml.
368
+ # These map directly to AgentConfig accessors.
369
+ CONFIG_SETTINGS_KEYS = %w[
370
+ enable_compression enable_prompt_caching memory_update_enabled
371
+ skill_evolution
372
+ ].freeze
373
+
374
+ # Serialize the current agent configuration to YAML.
375
+ # Outputs a hash with "settings" and "models" keys (new format).
376
+ # Backward compatibility: old flat-array format is still readable by .load.
342
377
  def to_yaml
343
- persistable = @models.reject { |m| m["auto_injected"] }.map do |m|
378
+ persistable_models = @models.reject { |m| m["auto_injected"] }.map do |m|
344
379
  m.reject { |k, _| RUNTIME_ONLY_FIELDS.include?(k) }
345
380
  end
346
- YAML.dump(persistable)
381
+ settings = {
382
+ "enable_compression" => @enable_compression,
383
+ "enable_prompt_caching" => @enable_prompt_caching,
384
+ "memory_update_enabled" => @memory_update_enabled,
385
+ "skill_evolution" => @skill_evolution
386
+ }
387
+ YAML.dump("settings" => settings, "models" => persistable_models)
347
388
  end
348
389
 
349
390
  # Check if any model is configured
data/lib/clacky/cli.rb CHANGED
@@ -905,6 +905,14 @@ module Clacky
905
905
  option :port, type: :numeric, default: 7070, desc: "Listen port (default: 7070)"
906
906
  option :brand_test, type: :boolean, default: false,
907
907
  desc: "Enable brand test mode: mock license activation without calling remote API"
908
+ option :no_compression, type: :boolean, default: false,
909
+ desc: "Disable message compression (saves tokens but may lose context)"
910
+ option :no_memory, type: :boolean, default: false,
911
+ desc: "Disable automatic memory updates"
912
+ option :no_caching, type: :boolean, default: false,
913
+ desc: "Disable prompt caching"
914
+ option :no_skill_evolution, type: :boolean, default: false,
915
+ desc: "Disable automatic skill evolution"
908
916
  def server
909
917
  # ── Security gate ──────────────────────────────────────────────────────
910
918
  # Binding to 0.0.0.0 exposes the server to the public network.
@@ -948,6 +956,15 @@ module Clacky
948
956
  agent_config = Clacky::AgentConfig.load
949
957
  agent_config.permission_mode = :confirm_all
950
958
 
959
+ # Apply CLI overrides to agent config (--no-compression etc.)
960
+ # These override whatever is stored in config.yml.
961
+ agent_config.enable_compression = false if options[:no_compression]
962
+ agent_config.memory_update_enabled = false if options[:no_memory]
963
+ agent_config.enable_prompt_caching = false if options[:no_caching]
964
+ if options[:no_skill_evolution]
965
+ agent_config.skill_evolution[:enabled] = false
966
+ end
967
+
951
968
  client_factory = lambda do
952
969
  Clacky::Client.new(
953
970
  agent_config.api_key,
@@ -987,6 +1004,10 @@ module Clacky
987
1004
 
988
1005
  extra_flags = []
989
1006
  extra_flags << "--brand-test" if options[:brand_test]
1007
+ extra_flags << "--no-compression" if options[:no_compression]
1008
+ extra_flags << "--no-memory" if options[:no_memory]
1009
+ extra_flags << "--no-caching" if options[:no_caching]
1010
+ extra_flags << "--no-skill-evolution" if options[:no_skill_evolution]
990
1011
 
991
1012
  Clacky::Logger.console = true
992
1013
 
@@ -78,7 +78,7 @@ module Clacky
78
78
  emit("token_usage", **token_data)
79
79
  end
80
80
 
81
- def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false)
81
+ def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil)
82
82
  data = { iterations: iterations, cost: cost }
83
83
  data[:duration] = duration if duration
84
84
  data[:cache_stats] = cache_stats if cache_stats
@@ -19,14 +19,26 @@ module Clacky
19
19
  # Detect if the request should use the Bedrock Converse API.
20
20
  # Matches any of:
21
21
  # - API key with "ABSK" prefix (native AWS Bedrock)
22
- # - API key with "clacky-" prefix (Clacky workspace key, proxied via Bedrock Converse)
23
22
  # - Model ID with "abs-" prefix (Clacky AI proxy that speaks Bedrock Converse)
23
+ #
24
+ # A bare "clacky-" key is NOT enough: that same workspace key is also
25
+ # used for dsk-*, or-*, and other OpenAI-compatible aliases served by
26
+ # the same Clacky proxy on a different endpoint. The *model prefix* is
27
+ # the source of truth for which upstream format the proxy expects:
28
+ #
29
+ # abs-* → Bedrock Converse (POST /model/{id}/converse)
30
+ # dsk-* → OpenAI-compatible (POST /chat/completions)
31
+ # or-* → OpenAI-compatible (POST /chat/completions)
32
+ # other → depends on base_url + explicit anthropic_format flag
33
+ #
34
+ # Historically this method also returned true for any "clacky-" key,
35
+ # which forced non-abs aliases into the Bedrock endpoint and produced
36
+ # `unknown model "..."` errors. Keep the explicit-prefix rule: if you
37
+ # add a new OpenAI-compatible alias family on the Clacky proxy, it
38
+ # will route correctly without touching this file.
24
39
  def self.bedrock_api_key?(api_key, model)
25
- # dsk- prefixed models use the OpenAI-compatible /chat/completions endpoint
26
- # on the same Clacky proxy, not the Bedrock Converse /model/{model}/converse path.
27
- return false if model.to_s.start_with?("dsk-")
28
-
29
- api_key.to_s.start_with?("ABSK", "clacky-") || model.to_s.start_with?("abs-")
40
+ return true if api_key.to_s.start_with?("ABSK")
41
+ model.to_s.start_with?("abs-")
30
42
  end
31
43
 
32
44
  module_function
@@ -97,7 +97,7 @@ module Clacky
97
97
  puts_line("[shell] #{command}")
98
98
  end
99
99
 
100
- def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false)
100
+ def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil)
101
101
  parts = ["[done] iterations=#{iterations}", "cost=$#{cost.round(4)}"]
102
102
  parts << "duration=#{duration.round(1)}s" if duration
103
103
  puts_line(parts.join(" "))
@@ -22,7 +22,7 @@ module Clacky
22
22
  "name" => "OpenClacky",
23
23
  "base_url" => "https://api.openclacky.com",
24
24
  "api" => "bedrock",
25
- "default_model" => "abs-claude-sonnet-4-5",
25
+ "default_model" => "abs-claude-sonnet-4-6",
26
26
  "models" => [
27
27
  "abs-claude-opus-4-7",
28
28
  "abs-claude-opus-4-6",
@@ -30,12 +30,15 @@ module Clacky
30
30
  "abs-claude-sonnet-4-5",
31
31
  "abs-claude-haiku-4-5",
32
32
  "dsk-deepseek-v4-pro",
33
- "dsk-deepseek-v4-flash"
33
+ "dsk-deepseek-v4-flash",
34
+ "or-gemini-3-1-pro"
34
35
  ],
35
36
  # Provider-level default: the Claude family served here is vision-capable.
36
37
  "capabilities" => { "vision" => true }.freeze,
37
38
  # Model-level overrides: DeepSeek models routed through this provider
38
39
  # are text-only; images uploaded for them must be downgraded to disk refs.
40
+ # Gemini 3.1 Pro keeps the provider-default vision=true (it accepts
41
+ # image/audio/video input natively via OpenRouter).
39
42
  "model_capabilities" => {
40
43
  "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
41
44
  "dsk-deepseek-v4-flash" => { "vision" => false }.freeze
@@ -46,6 +49,10 @@ module Clacky
46
49
  # weak models (haiku / v4-flash) ARE the lite tier themselves, so
47
50
  # they're intentionally not listed here — no injection happens when
48
51
  # the default model is already lite-class.
52
+ #
53
+ # or-gemini-3-1-pro is intentionally absent: Gemini has no lite
54
+ # sibling wired up (yet) on this provider; subagents using the
55
+ # Gemini default will just reuse it for lite work until we add one.
49
56
  "lite_models" => {
50
57
  "abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
51
58
  "abs-claude-opus-4-6" => "abs-claude-haiku-4-5",
@@ -86,8 +93,6 @@ module Clacky
86
93
  "models" => [
87
94
  "deepseek-v4-flash",
88
95
  "deepseek-v4-pro",
89
- "deepseek-chat",
90
- "deepseek-reasoner"
91
96
  ],
92
97
  # DeepSeek V4 API does not accept image inputs — text-only across all models.
93
98
  "capabilities" => { "vision" => false }.freeze,
@@ -137,9 +142,11 @@ module Clacky
137
142
  "abs-claude-sonnet-4-5",
138
143
  "abs-claude-haiku-4-5",
139
144
  "dsk-deepseek-v4-pro",
140
- "dsk-deepseek-v4-flash"
145
+ "dsk-deepseek-v4-flash",
146
+ "or-gemini-3-1-pro"
141
147
  ],
142
- # Same lineup as openclacky — Claude is vision, DeepSeek is text-only.
148
+ # Same lineup as openclacky — Claude is vision, DeepSeek is text-only,
149
+ # Gemini inherits the provider-default vision=true.
143
150
  "capabilities" => { "vision" => true }.freeze,
144
151
  "model_capabilities" => {
145
152
  "dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
@@ -104,10 +104,14 @@ module Clacky
104
104
  # Suppress
105
105
  end
106
106
 
107
- def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false)
107
+ def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil)
108
108
  flush_buffer
109
109
  parts = ["Done", "#{iterations} step#{"s" if iterations != 1}"]
110
- parts << "$#{cost.round(4)}" if cost && cost > 0
110
+ # Only show cost when pricing source is known (model matched pricing table).
111
+ # Unknown models return nil — skip to avoid misleading numbers.
112
+ if cost && cost > 0 && cost_source
113
+ parts << "$#{cost.round(4)}"
114
+ end
111
115
  parts << "#{duration.round(1)}s" if duration
112
116
  send_text(parts.join(" · "))
113
117
  end