openclacky 1.5.1 → 1.5.3
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/CHANGELOG.md +48 -0
- data/lib/clacky/agent/llm_caller.rb +48 -0
- data/lib/clacky/agent/message_compressor_helper.rb +7 -0
- data/lib/clacky/agent/session_serializer.rb +42 -0
- data/lib/clacky/agent/skill_manager.rb +7 -0
- data/lib/clacky/agent.rb +58 -1
- data/lib/clacky/agent_config.rb +71 -1
- data/lib/clacky/brand_config.rb +88 -59
- data/lib/clacky/client.rb +13 -2
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +15 -3
- data/lib/clacky/extension/packager.rb +17 -0
- data/lib/clacky/locales/en.rb +4 -2
- data/lib/clacky/locales/zh.rb +5 -3
- data/lib/clacky/message_format/anthropic.rb +56 -1
- data/lib/clacky/message_format/open_ai.rb +69 -4
- data/lib/clacky/message_history.rb +15 -1
- data/lib/clacky/providers.rb +73 -6
- data/lib/clacky/server/http_server.rb +61 -29
- data/lib/clacky/server/session_registry.rb +4 -2
- data/lib/clacky/skill.rb +4 -0
- data/lib/clacky/tools/invoke_skill.rb +21 -0
- data/lib/clacky/tools/security.rb +11 -13
- data/lib/clacky/ui_interface.rb +2 -0
- data/lib/clacky/utils/model_pricing.rb +8 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +210 -21
- data/lib/clacky/web/app.js +8 -3
- data/lib/clacky/web/components/code-editor.js +50 -12
- data/lib/clacky/web/features/billing/view.js +7 -1
- data/lib/clacky/web/features/extensions/store.js +23 -7
- data/lib/clacky/web/features/extensions/view.js +8 -4
- data/lib/clacky/web/features/new-session/view.js +61 -0
- data/lib/clacky/web/features/workspace/store.js +9 -0
- data/lib/clacky/web/features/workspace/view.js +8 -1
- data/lib/clacky/web/i18n.js +14 -0
- data/lib/clacky/web/index.html +4 -2
- data/lib/clacky/web/settings.js +49 -14
- metadata +2 -2
|
@@ -103,6 +103,15 @@ module Clacky
|
|
|
103
103
|
question: question, context: context, options: options }
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
+
def show_subagent_start(skill: nil, iterations: nil, cost_usd: nil)
|
|
107
|
+
@events << { type: "subagent_start", session_id: @session_id,
|
|
108
|
+
skill: skill, iterations: iterations, cost_usd: cost_usd }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def show_subagent_end
|
|
112
|
+
@events << { type: "subagent_end", session_id: @session_id }
|
|
113
|
+
end
|
|
114
|
+
|
|
106
115
|
# Ignore all other UI methods (progress, errors, etc.) during history replay
|
|
107
116
|
def method_missing(name, *args, **kwargs); end
|
|
108
117
|
def respond_to_missing?(name, include_private = false); true; end
|
|
@@ -2267,19 +2276,23 @@ module Clacky
|
|
|
2267
2276
|
|
|
2268
2277
|
Clacky::Logger.debug("[Brand] api_brand_status: expired=#{brand.expired?} grace_exceeded=#{brand.grace_period_exceeded?} expires_at=#{brand.license_expires_at&.iso8601 || "nil"}")
|
|
2269
2278
|
|
|
2270
|
-
warning
|
|
2279
|
+
warning = nil
|
|
2280
|
+
warning_type = nil
|
|
2271
2281
|
if brand.expired?
|
|
2272
|
-
warning
|
|
2282
|
+
warning = "Your #{brand.product_name} license has expired. Please renew to continue."
|
|
2283
|
+
warning_type = "expired"
|
|
2273
2284
|
elsif brand.grace_period_exceeded?
|
|
2274
|
-
warning
|
|
2285
|
+
warning = "License server unreachable for more than 3 days. Please check your connection."
|
|
2286
|
+
warning_type = "unreachable"
|
|
2275
2287
|
elsif brand.license_expires_at && !brand.expired?
|
|
2276
2288
|
days_remaining = ((brand.license_expires_at - Time.now.utc) / 86_400).ceil
|
|
2277
2289
|
if days_remaining <= 7
|
|
2278
|
-
warning
|
|
2290
|
+
warning = "Your #{brand.product_name} license expires in #{days_remaining} day#{"s" if days_remaining != 1}. Please renew soon."
|
|
2291
|
+
warning_type = "expiring"
|
|
2279
2292
|
end
|
|
2280
2293
|
end
|
|
2281
2294
|
|
|
2282
|
-
Clacky::Logger.debug("[Brand] api_brand_status: warning=#{warning.inspect}")
|
|
2295
|
+
Clacky::Logger.debug("[Brand] api_brand_status: warning=#{warning.inspect} warning_type=#{warning_type.inspect}")
|
|
2283
2296
|
|
|
2284
2297
|
json_response(res, 200, {
|
|
2285
2298
|
branded: true,
|
|
@@ -2288,6 +2301,7 @@ module Clacky
|
|
|
2288
2301
|
homepage_url: brand.homepage_url,
|
|
2289
2302
|
logo_url: brand.logo_url,
|
|
2290
2303
|
warning: warning,
|
|
2304
|
+
warning_type: warning_type,
|
|
2291
2305
|
test_mode: @brand_test,
|
|
2292
2306
|
user_licensed: brand.user_licensed?,
|
|
2293
2307
|
license_user_id: brand.license_user_id
|
|
@@ -2478,11 +2492,11 @@ module Clacky
|
|
|
2478
2492
|
"origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
|
|
2479
2493
|
"hub_active" => market&.dig("hub_active"),
|
|
2480
2494
|
"download_count" => market&.dig("download_count").to_i,
|
|
2481
|
-
#
|
|
2482
|
-
#
|
|
2483
|
-
#
|
|
2484
|
-
#
|
|
2485
|
-
"unlisted" => market.nil?
|
|
2495
|
+
# Mark as unlisted when:
|
|
2496
|
+
# - market is nil (extension no longer exists on the platform), OR
|
|
2497
|
+
# - platform batch API explicitly returned unlisted:true (brand-private
|
|
2498
|
+
# extension removed from all distributions but not yet soft-deleted).
|
|
2499
|
+
"unlisted" => market.nil? || market["unlisted"] == true,
|
|
2486
2500
|
"layer" => container[:layer].to_s,
|
|
2487
2501
|
"installed" => true,
|
|
2488
2502
|
"removable" => true,
|
|
@@ -2541,10 +2555,11 @@ module Clacky
|
|
|
2541
2555
|
units
|
|
2542
2556
|
end
|
|
2543
2557
|
|
|
2544
|
-
# GET /api/store/extension?id=<slug-or-id>
|
|
2558
|
+
# GET /api/store/extension?id=<slug-or-id>[&source=brand]
|
|
2545
2559
|
#
|
|
2546
|
-
#
|
|
2547
|
-
#
|
|
2560
|
+
# Detail for a single extension. Pass source=brand to query brand-private
|
|
2561
|
+
# extensions via the license-gated API; omit (or any other value) for the
|
|
2562
|
+
# public marketplace API.
|
|
2548
2563
|
def api_store_extension_detail(req, res)
|
|
2549
2564
|
id = req.query["id"].to_s
|
|
2550
2565
|
if id.strip.empty?
|
|
@@ -2553,13 +2568,10 @@ module Clacky
|
|
|
2553
2568
|
end
|
|
2554
2569
|
|
|
2555
2570
|
brand = Clacky::BrandConfig.load
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
result = brand.extension_detail!(id)
|
|
2561
|
-
if !result[:success] && brand.activated?
|
|
2562
|
-
result = brand.brand_extension_detail!(id)
|
|
2571
|
+
result = if req.query["source"] == "brand"
|
|
2572
|
+
brand.brand_extension_detail!(id)
|
|
2573
|
+
else
|
|
2574
|
+
brand.extension_detail!(id)
|
|
2563
2575
|
end
|
|
2564
2576
|
|
|
2565
2577
|
if result[:success] && result[:extension]
|
|
@@ -2570,7 +2582,7 @@ module Clacky
|
|
|
2570
2582
|
"installed" => !container.nil?,
|
|
2571
2583
|
"installed_version" => container&.dig(:version),
|
|
2572
2584
|
"removable" => container && container[:layer] == :installed,
|
|
2573
|
-
"disabled" => container ? container[:disabled] == true : false
|
|
2585
|
+
"disabled" => container ? container[:disabled] == true : false,
|
|
2574
2586
|
)
|
|
2575
2587
|
json_response(res, 200, { ok: true, extension: ext })
|
|
2576
2588
|
else
|
|
@@ -2597,9 +2609,7 @@ module Clacky
|
|
|
2597
2609
|
"homepage" => market ? (market["homepage"] || "") : container[:homepage].to_s,
|
|
2598
2610
|
"origin" => market ? (market["origin"] || container[:origin]) : container[:origin],
|
|
2599
2611
|
"hub_active" => market&.dig("hub_active"),
|
|
2600
|
-
|
|
2601
|
-
# marketplace (origin != "self") but is no longer listed there.
|
|
2602
|
-
"unlisted" => market.nil? && container[:origin].to_s != "self",
|
|
2612
|
+
"unlisted" => market.nil? || market["unlisted"] == true,
|
|
2603
2613
|
"installed" => true,
|
|
2604
2614
|
"removable" => true,
|
|
2605
2615
|
"disabled" => container[:disabled] == true,
|
|
@@ -3773,9 +3783,10 @@ module Clacky
|
|
|
3773
3783
|
|
|
3774
3784
|
# POST /api/file-action
|
|
3775
3785
|
# Unified file action endpoint — open locally or download.
|
|
3776
|
-
# Body: { path: String, action: "open" | "download" }
|
|
3786
|
+
# Body: { path: String, action: "open" | "download" | "save" }
|
|
3777
3787
|
# open: opens the file with the OS default handler (local deployments).
|
|
3778
3788
|
# download: returns the file as a download (remote deployments).
|
|
3789
|
+
# save: writes content back to the file. Body must include { content: String }.
|
|
3779
3790
|
def api_file_action(req, res)
|
|
3780
3791
|
body = parse_json_body(req)
|
|
3781
3792
|
path = body["path"]
|
|
@@ -3789,7 +3800,10 @@ module Clacky
|
|
|
3789
3800
|
linux_path = Utils::EnvironmentDetector.win_to_linux_path(path)
|
|
3790
3801
|
linux_path = File.expand_path(linux_path)
|
|
3791
3802
|
|
|
3792
|
-
|
|
3803
|
+
# For save action, the file may not exist yet — skip the existence check.
|
|
3804
|
+
unless action == "save"
|
|
3805
|
+
return json_response(res, 404, { error: "file not found" }) unless File.exist?(linux_path)
|
|
3806
|
+
end
|
|
3793
3807
|
|
|
3794
3808
|
case action
|
|
3795
3809
|
when "open"
|
|
@@ -3804,8 +3818,14 @@ module Clacky
|
|
|
3804
3818
|
when "display-path"
|
|
3805
3819
|
display = Utils::EnvironmentDetector.linux_to_win_path(linux_path)
|
|
3806
3820
|
json_response(res, 200, { ok: true, path: display })
|
|
3821
|
+
when "save"
|
|
3822
|
+
content = body["content"]
|
|
3823
|
+
return json_response(res, 400, { error: "content is required" }) if content.nil?
|
|
3824
|
+
FileUtils.mkdir_p(File.dirname(linux_path))
|
|
3825
|
+
File.write(linux_path, content, encoding: "UTF-8")
|
|
3826
|
+
json_response(res, 200, { ok: true })
|
|
3807
3827
|
else
|
|
3808
|
-
json_response(res, 400, { error: "invalid action. Must be 'open', 'reveal', 'download'
|
|
3828
|
+
json_response(res, 400, { error: "invalid action. Must be 'open', 'reveal', 'download', 'display-path' or 'save'" })
|
|
3809
3829
|
end
|
|
3810
3830
|
rescue => e
|
|
3811
3831
|
json_response(res, 500, { ok: false, error: e.message })
|
|
@@ -5398,6 +5418,7 @@ module Clacky
|
|
|
5398
5418
|
base_url: m["base_url"],
|
|
5399
5419
|
api_key_masked: mask_api_key(m["api_key"]),
|
|
5400
5420
|
anthropic_format: m["anthropic_format"] || false,
|
|
5421
|
+
provider_id: m["provider_id"],
|
|
5401
5422
|
type: m["type"]
|
|
5402
5423
|
}
|
|
5403
5424
|
end
|
|
@@ -5606,7 +5627,8 @@ module Clacky
|
|
|
5606
5627
|
"model" => model,
|
|
5607
5628
|
"base_url" => base_url,
|
|
5608
5629
|
"api_key" => api_key,
|
|
5609
|
-
"anthropic_format" => body["anthropic_format"] || false
|
|
5630
|
+
"anthropic_format" => body["anthropic_format"] || false,
|
|
5631
|
+
"provider_id" => body["provider_id"].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
5610
5632
|
}
|
|
5611
5633
|
type = body["type"].to_s
|
|
5612
5634
|
unless type.empty?
|
|
@@ -5667,6 +5689,14 @@ module Clacky
|
|
|
5667
5689
|
if body.key?("anthropic_format")
|
|
5668
5690
|
target["anthropic_format"] = !!body["anthropic_format"]
|
|
5669
5691
|
end
|
|
5692
|
+
if body.key?("provider_id")
|
|
5693
|
+
v = body["provider_id"].to_s.strip
|
|
5694
|
+
if v.empty?
|
|
5695
|
+
target.delete("provider_id")
|
|
5696
|
+
else
|
|
5697
|
+
target["provider_id"] = v
|
|
5698
|
+
end
|
|
5699
|
+
end
|
|
5670
5700
|
if body.key?("api_key")
|
|
5671
5701
|
new_key = body["api_key"].to_s
|
|
5672
5702
|
# Only store a real, unmasked, non-empty value. This is the
|
|
@@ -5973,7 +6003,9 @@ module Clacky
|
|
|
5973
6003
|
|
|
5974
6004
|
if model_name && !model_name.empty?
|
|
5975
6005
|
info = agent.current_model_info
|
|
5976
|
-
provider_id
|
|
6006
|
+
# Prefer explicitly saved provider_id, fall back to base_url lookup
|
|
6007
|
+
provider_id = info&.dig(:provider_id).to_s.strip.then { |v| v.empty? ? nil : v }
|
|
6008
|
+
provider_id ||= (info && Clacky::Providers.find_by_base_url(info[:base_url]))
|
|
5977
6009
|
allowed = provider_id ? Clacky::Providers.models(provider_id) : []
|
|
5978
6010
|
if allowed.empty?
|
|
5979
6011
|
return json_response(res, 400, { error: "Current model has no provider preset; sub-model switching unavailable" })
|
|
@@ -376,8 +376,10 @@ module Clacky
|
|
|
376
376
|
# in any preset (e.g. self-hosted custom endpoints) — the WebUI
|
|
377
377
|
# treats that as "no sub-model switcher available".
|
|
378
378
|
private def sub_model_options_for(model_info)
|
|
379
|
-
return [] unless model_info
|
|
380
|
-
provider_id
|
|
379
|
+
return [] unless model_info
|
|
380
|
+
# Prefer explicitly saved provider_id, fall back to base_url lookup
|
|
381
|
+
provider_id = model_info[:provider_id].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
382
|
+
provider_id ||= (model_info[:base_url] && Clacky::Providers.find_by_base_url(model_info[:base_url]))
|
|
381
383
|
return [] unless provider_id
|
|
382
384
|
Clacky::Providers.models(provider_id)
|
|
383
385
|
end
|
data/lib/clacky/skill.rb
CHANGED
|
@@ -537,6 +537,10 @@ module Clacky
|
|
|
537
537
|
name_invalid = !valid_slug.call(@name) || @name.length > 64
|
|
538
538
|
|
|
539
539
|
if name_invalid
|
|
540
|
+
# Preserve the original non-slug name as name_zh (display name) if not already set.
|
|
541
|
+
# e.g. name: "中英翻译" → name_zh = "中英翻译", name falls back to dir slug.
|
|
542
|
+
@name_zh ||= @name
|
|
543
|
+
|
|
540
544
|
if valid_slug.call(dir_slug)
|
|
541
545
|
# Recoverable: fall back to directory name, record a warning
|
|
542
546
|
@warnings << "Invalid name '#{@name}' in metadata; using directory name '#{dir_slug}' instead."
|
|
@@ -77,6 +77,27 @@ module Clacky
|
|
|
77
77
|
"InvokeSkill(#{skill})"
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
# Format the tool result for the LLM.
|
|
81
|
+
# Converts the raw Hash into a markdown string so the LLM can directly
|
|
82
|
+
# read the subagent output without parsing a JSON wrapper. JSON-encoding
|
|
83
|
+
# the result as a nested string caused models to miss the "result" field
|
|
84
|
+
# and hallucinate that the subagent returned nothing.
|
|
85
|
+
# @param result [Hash, String] Tool execution result
|
|
86
|
+
# @return [String] LLM-friendly formatted result
|
|
87
|
+
def format_result_for_llm(result)
|
|
88
|
+
if result.is_a?(String)
|
|
89
|
+
result
|
|
90
|
+
elsif result[:error]
|
|
91
|
+
"Error: #{result[:error]}"
|
|
92
|
+
elsif result[:skill_type] == "subagent"
|
|
93
|
+
subagent_result = result[:result].to_s.strip
|
|
94
|
+
subagent_result = "(subagent produced no output)" if subagent_result.empty?
|
|
95
|
+
"Subagent executed successfully.\n\n#{subagent_result}"
|
|
96
|
+
else
|
|
97
|
+
"Skill content expanded"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
80
101
|
# Format the tool result for display
|
|
81
102
|
# @param result [Hash] Tool execution result
|
|
82
103
|
# @return [String] Formatted result summary
|
|
@@ -19,17 +19,18 @@ module Clacky
|
|
|
19
19
|
# Responsibilities (applied to the `command` string BEFORE it is handed
|
|
20
20
|
# to a shell / PTY for execution):
|
|
21
21
|
#
|
|
22
|
-
# 1. Block hard-dangerous commands:
|
|
22
|
+
# 1. Block hard-dangerous commands: pkill clacky, eval, exec,
|
|
23
23
|
# `...`, | sh, | bash,
|
|
24
24
|
# redirect to /etc /usr /bin.
|
|
25
25
|
# 2. Rewrite `curl ... | bash` → save script to a file for manual
|
|
26
26
|
# review instead of exec.
|
|
27
|
-
# 3. Protect credential/secret files: .
|
|
27
|
+
# 3. Protect credential/secret files: .ssh/, .aws/ — block
|
|
28
28
|
# writes to these only. Other
|
|
29
29
|
# "project" files (Gemfile,
|
|
30
|
-
# README.md, package.json,
|
|
31
|
-
# are NOT protected —
|
|
32
|
-
# them is a normal dev
|
|
30
|
+
# README.md, package.json,
|
|
31
|
+
# .env, …) are NOT protected —
|
|
32
|
+
# editing them is a normal dev
|
|
33
|
+
# task.
|
|
33
34
|
#
|
|
34
35
|
# Note on `rm`:
|
|
35
36
|
# `rm` is NOT rewritten here — it's intercepted at runtime by a shell
|
|
@@ -135,7 +136,10 @@ module Clacky
|
|
|
135
136
|
when /^curl.*\|\s*(sh|bash)/
|
|
136
137
|
replace_curl_pipe_command(command)
|
|
137
138
|
when /^sudo\s+/
|
|
138
|
-
|
|
139
|
+
# sudo is allowed — the user is in control and takes responsibility.
|
|
140
|
+
# Still log it for audit trail.
|
|
141
|
+
log_warning("sudo command executed: #{command}")
|
|
142
|
+
command
|
|
139
143
|
when />\s*\/dev\/null\s*$/
|
|
140
144
|
allow_dev_null_redirect(command)
|
|
141
145
|
when /^(mv|cp|mkdir|touch|echo)\s+/
|
|
@@ -175,10 +179,6 @@ module Clacky
|
|
|
175
179
|
end
|
|
176
180
|
end
|
|
177
181
|
|
|
178
|
-
def block_sudo_command(_command)
|
|
179
|
-
raise SecurityError, "sudo commands are not allowed for security reasons"
|
|
180
|
-
end
|
|
181
|
-
|
|
182
182
|
def allow_dev_null_redirect(command)
|
|
183
183
|
command
|
|
184
184
|
end
|
|
@@ -273,8 +273,6 @@ module Clacky
|
|
|
273
273
|
SECRET_WRITE_PATTERNS = [
|
|
274
274
|
%r{(?:\A|/)\.ssh/},
|
|
275
275
|
%r{(?:\A|/)\.aws/},
|
|
276
|
-
/(?:\A|\/)\.env(?:\.|\z)/,
|
|
277
|
-
/\.env\z/
|
|
278
276
|
].freeze
|
|
279
277
|
|
|
280
278
|
def validate_secret_write(path)
|
|
@@ -319,7 +317,7 @@ module Clacky
|
|
|
319
317
|
end
|
|
320
318
|
|
|
321
319
|
private :replace_chmod_command,
|
|
322
|
-
:replace_curl_pipe_command,
|
|
320
|
+
:replace_curl_pipe_command,
|
|
323
321
|
:allow_dev_null_redirect, :validate_and_allow,
|
|
324
322
|
:validate_general_command,
|
|
325
323
|
:validate_file_path, :validate_secret_write,
|
data/lib/clacky/ui_interface.rb
CHANGED
|
@@ -11,6 +11,8 @@ module Clacky
|
|
|
11
11
|
# @param files [Array<Hash>] extracted file refs: [{ name:, path:, inline: }]
|
|
12
12
|
def show_assistant_message(content, files:); end
|
|
13
13
|
def show_feedback_request(question, context, options); end
|
|
14
|
+
def show_subagent_start(skill: nil, iterations: nil, cost_usd: nil); end
|
|
15
|
+
def show_subagent_end; end
|
|
14
16
|
def show_tool_call(name, args); end
|
|
15
17
|
def show_tool_result(result); end
|
|
16
18
|
def show_tool_stdout(lines); end
|
|
@@ -472,6 +472,12 @@ module Clacky
|
|
|
472
472
|
# endpoints don't charge separately for cache writes (Z.ai's page lists
|
|
473
473
|
# "Cached Input Storage: Limited-time Free"), so bill writes at the
|
|
474
474
|
# regular input miss rate for safe "displayed ≤ actual" behaviour.
|
|
475
|
+
"glm-5.2" => {
|
|
476
|
+
input: { default: 1.40, over_200k: 1.40 },
|
|
477
|
+
output: { default: 4.40, over_200k: 4.40 },
|
|
478
|
+
cache: { write: 1.40, read: 0.26 }
|
|
479
|
+
},
|
|
480
|
+
|
|
475
481
|
"glm-5.1" => {
|
|
476
482
|
input: { default: 1.40, over_200k: 1.40 },
|
|
477
483
|
output: { default: 4.40, over_200k: 4.40 },
|
|
@@ -786,6 +792,8 @@ module Clacky
|
|
|
786
792
|
# (mainland bigmodel.cn vs intl z.ai) the user configured.
|
|
787
793
|
# Strict anchored match so unrelated strings like "glm-5-x-foo"
|
|
788
794
|
# don't silently borrow a nearby model's rate.
|
|
795
|
+
when /^glm-5\.2$/i
|
|
796
|
+
"glm-5.2"
|
|
789
797
|
when /^glm-5\.1$/i
|
|
790
798
|
"glm-5.1"
|
|
791
799
|
when /^glm-5v-turbo$/i
|
data/lib/clacky/version.rb
CHANGED