openclacky 1.5.1 → 1.5.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/CHANGELOG.md +24 -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 +42 -0
- data/lib/clacky/brand_config.rb +88 -59
- data/lib/clacky/client.rb +6 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +5 -0
- 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 +29 -0
- data/lib/clacky/message_history.rb +15 -1
- data/lib/clacky/providers.rb +22 -0
- data/lib/clacky/server/http_server.rb +41 -17
- 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/version.rb +1 -1
- data/lib/clacky/web/app.css +61 -14
- data/lib/clacky/web/features/billing/view.js +5 -0
- data/lib/clacky/web/features/extensions/store.js +9 -2
- data/lib/clacky/web/features/new-session/view.js +61 -0
- data/lib/clacky/web/i18n.js +10 -0
- data/lib/clacky/web/index.html +3 -0
- data/lib/clacky/web/settings.js +41 -12
- metadata +2 -2
data/lib/clacky/locales/zh.rb
CHANGED
|
@@ -9,11 +9,12 @@ module Clacky
|
|
|
9
9
|
"llm.error.403.model_not_allowed" => "当前模型不支持免费试用,请升级套餐或切换其他模型",
|
|
10
10
|
"llm.error.403.api_key_revoked" => "API 密钥已被撤销,请前往控制台重新生成",
|
|
11
11
|
"llm.error.403.api_key_expired" => "API 密钥已过期,请前往控制台重新生成",
|
|
12
|
-
"llm.error.403.quota_exceeded" => "
|
|
12
|
+
"llm.error.403.quota_exceeded" => "API Key 配额已超限,请在管理后台调整配额或关闭限制",
|
|
13
13
|
"llm.error.403.access_denied" => "访问被拒绝,请检查 API 密钥权限",
|
|
14
14
|
"llm.error.403.default" => "访问被拒绝",
|
|
15
15
|
"llm.error.endpoint_not_found" => "API 端点不存在,请检查服务地址配置",
|
|
16
|
-
"llm.error.rate_limit_429" => "
|
|
16
|
+
"llm.error.rate_limit_429" => "请求过于频繁,请稍后重试",
|
|
17
|
+
"llm.error.quota_exhausted" => "API 配额已用尽(已达 Key 的用量上限),请前往控制台提升配额或升级套餐后继续",
|
|
17
18
|
"llm.error.server_error" => "服务暂时不可用(%<status>d),正在重试...",
|
|
18
19
|
"llm.error.unexpected" => "请求失败(%<status>d)",
|
|
19
20
|
"llm.error.html_response" => "服务暂时不可用(收到 HTML 错误页),正在重试...",
|
|
@@ -21,6 +22,7 @@ module Clacky
|
|
|
21
22
|
"llm.error.request_timeout" => "请求超时(已重试 %<retries>d 次)",
|
|
22
23
|
"llm.error.network_failed" => "网络连接失败(已重试 %<retries>d 次)",
|
|
23
24
|
"llm.error.service_unavailable" => "服务暂时不可用(已重试 %<retries>d 次)",
|
|
25
|
+
"llm.warn.switching_to_fallback_url" => "主节点无法连接,正在切换到备用节点:%<url>s",
|
|
24
26
|
"platform.error.invalid_proof" => "许可证密钥无效,请检查后重试。",
|
|
25
27
|
"platform.error.invalid_signature" => "请求签名无效。",
|
|
26
28
|
"platform.error.nonce_replayed" => "检测到重复请求,请重试。",
|
|
@@ -28,7 +30,7 @@ module Clacky
|
|
|
28
30
|
"platform.error.license_revoked" => "该许可证已被吊销,请联系客服。",
|
|
29
31
|
"platform.error.license_expired" => "该许可证已过期,请续订后继续。",
|
|
30
32
|
"platform.error.device_limit_reached" => "该许可证的设备数量已达上限。",
|
|
31
|
-
"platform.error.device_revoked" => "
|
|
33
|
+
"platform.error.device_revoked" => "该设备已从许可证中移除。如需重新绑定,请等待 15 分钟后再试。",
|
|
32
34
|
"platform.error.invalid_license" => "未找到许可证密钥,请核对后重试。",
|
|
33
35
|
"platform.error.device_not_found" => "设备未注册,请重新激活。",
|
|
34
36
|
"platform.error.contributor_required" => "发布扩展需要先成为扩展贡献者。请登录平台,打开「我的扩展」页面,点击「成为扩展贡献者」即可开通(无需审核)。",
|
|
@@ -66,6 +66,7 @@ module Clacky
|
|
|
66
66
|
system_text = system_messages.map { |m| extract_text(m[:content]) }.join("\n\n")
|
|
67
67
|
|
|
68
68
|
api_messages = regular_messages.map { |msg| to_api_message(msg, caching_enabled) }
|
|
69
|
+
api_messages = merge_consecutive_tool_results(api_messages)
|
|
69
70
|
api_tools = tools&.map { |t| to_api_tool(t) }
|
|
70
71
|
|
|
71
72
|
if caching_enabled && api_tools&.any?
|
|
@@ -90,6 +91,34 @@ module Clacky
|
|
|
90
91
|
%w[low medium high].include?(s) ? s : nil
|
|
91
92
|
end
|
|
92
93
|
|
|
94
|
+
# Merge consecutive tool_result user messages into a single user message.
|
|
95
|
+
# Anthropic requires all tool_results for an assistant turn to appear in
|
|
96
|
+
# a SINGLE user message immediately following the assistant. When an
|
|
97
|
+
# assistant calls multiple tools, the canonical format produces one
|
|
98
|
+
# role:"tool" message per result, which to_api_message converts into
|
|
99
|
+
# separate role:"user" messages — breaking the Anthropic contract.
|
|
100
|
+
private_class_method def self.merge_consecutive_tool_results(api_messages)
|
|
101
|
+
result = []
|
|
102
|
+
pending = nil
|
|
103
|
+
|
|
104
|
+
api_messages.each do |msg|
|
|
105
|
+
if msg[:role] == "user" && msg[:content].is_a?(Array) &&
|
|
106
|
+
msg[:content].any? { |b| b.is_a?(Hash) && b[:type] == "tool_result" }
|
|
107
|
+
if pending
|
|
108
|
+
pending[:content] += msg[:content]
|
|
109
|
+
else
|
|
110
|
+
pending = msg
|
|
111
|
+
result << pending
|
|
112
|
+
end
|
|
113
|
+
else
|
|
114
|
+
pending = nil
|
|
115
|
+
result << msg
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
result
|
|
120
|
+
end
|
|
121
|
+
|
|
93
122
|
# ── Response parsing ──────────────────────────────────────────────────────
|
|
94
123
|
|
|
95
124
|
# Parse Anthropic API response into canonical internal format.
|
|
@@ -10,7 +10,7 @@ module Clacky
|
|
|
10
10
|
# Fields that are internal to the agent and must not be sent to the API.
|
|
11
11
|
INTERNAL_FIELDS = %i[
|
|
12
12
|
task_id created_at system_injected session_context memory_update
|
|
13
|
-
subagent_instructions subagent_result token_usage
|
|
13
|
+
subagent_instructions subagent_result subagent_transcript token_usage
|
|
14
14
|
compressed_summary chunk_path truncated transient
|
|
15
15
|
chunk_index chunk_count
|
|
16
16
|
].freeze
|
|
@@ -71,6 +71,20 @@ module Clacky
|
|
|
71
71
|
self
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# Attach a key/value onto the most recent tool result message matching the
|
|
75
|
+
# given tool_call_id. Handles both OpenAI-style (role:"tool", tool_call_id)
|
|
76
|
+
# and Anthropic-style (role:"user" with tool_result blocks) messages.
|
|
77
|
+
# No-op if no matching message is found.
|
|
78
|
+
def attach_to_tool_result(tool_call_id, key, value)
|
|
79
|
+
msg = @messages.reverse.find do |m|
|
|
80
|
+
(m[:role] == "tool" && m[:tool_call_id] == tool_call_id) ||
|
|
81
|
+
(m[:role] == "user" && m[:content].is_a?(Array) &&
|
|
82
|
+
m[:content].any? { |b| b.is_a?(Hash) && b[:type] == "tool_result" && b[:tool_use_id] == tool_call_id })
|
|
83
|
+
end
|
|
84
|
+
msg[key] = value if msg
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
74
88
|
# Mutate the last message matching the predicate lambda in-place.
|
|
75
89
|
# Used by execute_skill_with_subagent to update instruction messages.
|
|
76
90
|
def mutate_last_matching(predicate, &block)
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -148,6 +148,18 @@ module Clacky
|
|
|
148
148
|
"abs-claude-fable-5" => "abs-claude-opus-4-8",
|
|
149
149
|
"abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5"
|
|
150
150
|
},
|
|
151
|
+
# Secondary gateway URL used when the primary base_url is unreachable
|
|
152
|
+
# after max retries. The model name stays the same — only the endpoint
|
|
153
|
+
# changes. Nil / absent means no URL fallback for this provider.
|
|
154
|
+
"fallback_base_url" => "https://llm.1024code.com",
|
|
155
|
+
# Two selectable endpoints exposed in the Base URL dropdown:
|
|
156
|
+
# Primary — global CDN, lowest latency for most regions.
|
|
157
|
+
# Secondary — China-optimised relay (1024code.com), useful when
|
|
158
|
+
# the primary is unreachable from mainland China.
|
|
159
|
+
"endpoint_variants" => [
|
|
160
|
+
{ "label" => "Primary (Global)", "label_key" => "settings.models.baseurl.variant.openclacky_primary", "base_url" => "https://api.openclacky.com" }.freeze,
|
|
161
|
+
{ "label" => "Secondary (China)", "label_key" => "settings.models.baseurl.variant.openclacky_secondary", "base_url" => "https://llm.1024code.com" }.freeze
|
|
162
|
+
].freeze,
|
|
151
163
|
"website_url" => "https://www.openclacky.com/ai-keys"
|
|
152
164
|
}.freeze,
|
|
153
165
|
|
|
@@ -779,6 +791,16 @@ module Clacky
|
|
|
779
791
|
preset&.dig("fallback_models", model)
|
|
780
792
|
end
|
|
781
793
|
|
|
794
|
+
# Get the fallback base URL for a provider (used when primary endpoint is unreachable).
|
|
795
|
+
# Returns nil if the provider has no secondary gateway configured.
|
|
796
|
+
# @param provider_id [String] The provider identifier
|
|
797
|
+
# @return [String, nil] The fallback base URL or nil
|
|
798
|
+
def fallback_base_url(provider_id)
|
|
799
|
+
preset = PRESETS[provider_id]
|
|
800
|
+
url = preset&.dig("fallback_base_url")
|
|
801
|
+
url&.empty? ? nil : url
|
|
802
|
+
end
|
|
803
|
+
|
|
782
804
|
# Find provider ID by base URL.
|
|
783
805
|
# Matches if the given URL starts with the provider's base_url (after normalisation),
|
|
784
806
|
# so both exact matches and sub-path variants (e.g. "/v1") are recognised.
|
|
@@ -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
|
|
@@ -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]
|
|
@@ -5398,6 +5410,7 @@ module Clacky
|
|
|
5398
5410
|
base_url: m["base_url"],
|
|
5399
5411
|
api_key_masked: mask_api_key(m["api_key"]),
|
|
5400
5412
|
anthropic_format: m["anthropic_format"] || false,
|
|
5413
|
+
provider_id: m["provider_id"],
|
|
5401
5414
|
type: m["type"]
|
|
5402
5415
|
}
|
|
5403
5416
|
end
|
|
@@ -5606,7 +5619,8 @@ module Clacky
|
|
|
5606
5619
|
"model" => model,
|
|
5607
5620
|
"base_url" => base_url,
|
|
5608
5621
|
"api_key" => api_key,
|
|
5609
|
-
"anthropic_format" => body["anthropic_format"] || false
|
|
5622
|
+
"anthropic_format" => body["anthropic_format"] || false,
|
|
5623
|
+
"provider_id" => body["provider_id"].to_s.strip.then { |v| v.empty? ? nil : v }
|
|
5610
5624
|
}
|
|
5611
5625
|
type = body["type"].to_s
|
|
5612
5626
|
unless type.empty?
|
|
@@ -5667,6 +5681,14 @@ module Clacky
|
|
|
5667
5681
|
if body.key?("anthropic_format")
|
|
5668
5682
|
target["anthropic_format"] = !!body["anthropic_format"]
|
|
5669
5683
|
end
|
|
5684
|
+
if body.key?("provider_id")
|
|
5685
|
+
v = body["provider_id"].to_s.strip
|
|
5686
|
+
if v.empty?
|
|
5687
|
+
target.delete("provider_id")
|
|
5688
|
+
else
|
|
5689
|
+
target["provider_id"] = v
|
|
5690
|
+
end
|
|
5691
|
+
end
|
|
5670
5692
|
if body.key?("api_key")
|
|
5671
5693
|
new_key = body["api_key"].to_s
|
|
5672
5694
|
# Only store a real, unmasked, non-empty value. This is the
|
|
@@ -5973,7 +5995,9 @@ module Clacky
|
|
|
5973
5995
|
|
|
5974
5996
|
if model_name && !model_name.empty?
|
|
5975
5997
|
info = agent.current_model_info
|
|
5976
|
-
provider_id
|
|
5998
|
+
# Prefer explicitly saved provider_id, fall back to base_url lookup
|
|
5999
|
+
provider_id = info&.dig(:provider_id).to_s.strip.then { |v| v.empty? ? nil : v }
|
|
6000
|
+
provider_id ||= (info && Clacky::Providers.find_by_base_url(info[:base_url]))
|
|
5977
6001
|
allowed = provider_id ? Clacky::Providers.models(provider_id) : []
|
|
5978
6002
|
if allowed.empty?
|
|
5979
6003
|
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
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -2173,6 +2173,40 @@ body {
|
|
|
2173
2173
|
margin-top: -0.625rem;
|
|
2174
2174
|
padding-left: 0.125rem;
|
|
2175
2175
|
}
|
|
2176
|
+
|
|
2177
|
+
/* ── Starter Prompts ── */
|
|
2178
|
+
.new-session-starter-prompts {
|
|
2179
|
+
margin-top: 0.25rem;
|
|
2180
|
+
}
|
|
2181
|
+
.ns-starter-label {
|
|
2182
|
+
font-size: 0.75rem;
|
|
2183
|
+
color: var(--color-text-tertiary);
|
|
2184
|
+
margin-bottom: 0.5rem;
|
|
2185
|
+
}
|
|
2186
|
+
.ns-starter-list {
|
|
2187
|
+
display: flex;
|
|
2188
|
+
flex-direction: column;
|
|
2189
|
+
gap: 0.375rem;
|
|
2190
|
+
}
|
|
2191
|
+
.ns-starter-item {
|
|
2192
|
+
display: block;
|
|
2193
|
+
width: 100%;
|
|
2194
|
+
text-align: left;
|
|
2195
|
+
padding: 0.5rem 0.75rem;
|
|
2196
|
+
border: 1px solid var(--color-border-primary);
|
|
2197
|
+
border-radius: 0.5rem;
|
|
2198
|
+
background: var(--color-bg-primary);
|
|
2199
|
+
color: var(--color-text-secondary);
|
|
2200
|
+
font-size: 0.8125rem;
|
|
2201
|
+
line-height: 1.45;
|
|
2202
|
+
cursor: pointer;
|
|
2203
|
+
transition: border-color 0.14s, background-color 0.14s, color 0.14s;
|
|
2204
|
+
}
|
|
2205
|
+
.ns-starter-item:hover {
|
|
2206
|
+
border-color: color-mix(in srgb, var(--color-accent-primary) 40%, var(--color-border-primary));
|
|
2207
|
+
background: var(--color-bg-hover);
|
|
2208
|
+
color: var(--color-text-primary);
|
|
2209
|
+
}
|
|
2176
2210
|
.ns-panels-label {
|
|
2177
2211
|
width: 100%;
|
|
2178
2212
|
font-size: 0.75rem;
|
|
@@ -2637,7 +2671,6 @@ body {
|
|
|
2637
2671
|
.session-banner { flex-shrink: 0; }
|
|
2638
2672
|
.session-composer {
|
|
2639
2673
|
flex-shrink: 0;
|
|
2640
|
-
margin-bottom: -9px;
|
|
2641
2674
|
display: flex;
|
|
2642
2675
|
padding-inline: 24px;
|
|
2643
2676
|
}
|
|
@@ -8967,10 +9000,11 @@ body.setup-mode[data-theme="dark"] {
|
|
|
8967
9000
|
padding-top: 0.75rem;
|
|
8968
9001
|
border-top: 1px solid var(--color-border-primary);
|
|
8969
9002
|
}
|
|
8970
|
-
.badge-active
|
|
8971
|
-
.badge-expired
|
|
8972
|
-
.badge-expiring
|
|
8973
|
-
.badge-
|
|
9003
|
+
.badge-active { color: var(--color-success); }
|
|
9004
|
+
.badge-expired { color: var(--color-error); }
|
|
9005
|
+
.badge-expiring { color: var(--color-warning); }
|
|
9006
|
+
.badge-unreachable { color: var(--color-text-secondary); }
|
|
9007
|
+
.badge-inactive { color: var(--color-text-secondary); }
|
|
8974
9008
|
|
|
8975
9009
|
/* Support QR code */
|
|
8976
9010
|
.brand-support-contact {
|
|
@@ -10908,6 +10942,19 @@ body.setup-mode[data-theme="dark"] {
|
|
|
10908
10942
|
flex-wrap: wrap;
|
|
10909
10943
|
}
|
|
10910
10944
|
|
|
10945
|
+
/* ── Disclaimer notice ──────────────────────────────────────────────── */
|
|
10946
|
+
.billing-disclaimer {
|
|
10947
|
+
display: flex;
|
|
10948
|
+
align-items: center;
|
|
10949
|
+
gap: 0.4rem;
|
|
10950
|
+
font-size: 0.75rem;
|
|
10951
|
+
color: var(--color-text-tertiary);
|
|
10952
|
+
padding: 0.4rem 0.75rem;
|
|
10953
|
+
background: var(--color-bg-secondary);
|
|
10954
|
+
border: 1px solid var(--color-border-primary);
|
|
10955
|
+
border-radius: 6px;
|
|
10956
|
+
}
|
|
10957
|
+
|
|
10911
10958
|
/* ── Period Button Group ─────────────────────────────────────────────── */
|
|
10912
10959
|
.billing-period-group {
|
|
10913
10960
|
display: flex;
|
|
@@ -11084,10 +11131,10 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11084
11131
|
justify-content: center;
|
|
11085
11132
|
flex-shrink: 0;
|
|
11086
11133
|
}
|
|
11087
|
-
.billing-stat-icon-cost
|
|
11088
|
-
.billing-stat-icon-tokens { background: color-mix(in srgb, #
|
|
11134
|
+
.billing-stat-icon-cost { background: color-mix(in srgb, var(--color-accent-primary) 10%, transparent); color: var(--color-accent-primary); }
|
|
11135
|
+
.billing-stat-icon-tokens { background: color-mix(in srgb, #2075ffbf 10%, transparent); color: #2075ffbf; }
|
|
11089
11136
|
.billing-stat-icon-requests { background: color-mix(in srgb, #f59e0b 10%, transparent); color: #f59e0b; }
|
|
11090
|
-
.billing-stat-icon-cache { background: color-mix(in srgb,
|
|
11137
|
+
.billing-stat-icon-cache { background: color-mix(in srgb, #10b981 10%, transparent); color: #10b981; }
|
|
11091
11138
|
.billing-stat-icon svg {
|
|
11092
11139
|
width: 18px;
|
|
11093
11140
|
height: 18px;
|
|
@@ -11318,7 +11365,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11318
11365
|
align-items: stretch;
|
|
11319
11366
|
}
|
|
11320
11367
|
.billing-cache-hit {
|
|
11321
|
-
background: #
|
|
11368
|
+
background: #2075ffbf;
|
|
11322
11369
|
width: 100%;
|
|
11323
11370
|
border-radius: 2px 2px 0 0;
|
|
11324
11371
|
transition: height 0.2s ease;
|
|
@@ -11346,7 +11393,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11346
11393
|
|
|
11347
11394
|
/* Legend colors — indigo + emerald */
|
|
11348
11395
|
.billing-legend-total { background: var(--color-accent-primary); }
|
|
11349
|
-
.billing-legend-cache-hit { background: #
|
|
11396
|
+
.billing-legend-cache-hit { background: #2075ffbf; }
|
|
11350
11397
|
.billing-legend-cache-miss{ background: #f59e0b; }
|
|
11351
11398
|
.billing-legend-output { background: #34d399; }
|
|
11352
11399
|
|
|
@@ -11397,7 +11444,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11397
11444
|
flex-shrink: 0;
|
|
11398
11445
|
}
|
|
11399
11446
|
.tooltip-total { background: var(--color-accent-primary); }
|
|
11400
|
-
.tooltip-cache-hit { background: #
|
|
11447
|
+
.tooltip-cache-hit { background: #2075ffbf; }
|
|
11401
11448
|
.tooltip-cache-miss{ background: #f59e0b; }
|
|
11402
11449
|
.tooltip-output { background: #34d399; }.tooltip-label {
|
|
11403
11450
|
flex: 1;
|
|
@@ -11472,7 +11519,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11472
11519
|
background: #34d399;
|
|
11473
11520
|
}
|
|
11474
11521
|
.billing-bar-cache-read {
|
|
11475
|
-
background: #
|
|
11522
|
+
background: #2075ffbf;
|
|
11476
11523
|
}
|
|
11477
11524
|
.billing-bar-cache-miss {
|
|
11478
11525
|
background: #f59e0b;
|
|
@@ -11951,7 +11998,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11951
11998
|
}
|
|
11952
11999
|
|
|
11953
12000
|
.swatch-indigo { background: #4f46e5; }
|
|
11954
|
-
.swatch-aurora-blue { background: #
|
|
12001
|
+
.swatch-aurora-blue { background: #2075ffbf; }
|
|
11955
12002
|
.swatch-forest-green { background: #10B981; }
|
|
11956
12003
|
.swatch-sunrise-orange { background: #F59E0B; }
|
|
11957
12004
|
.swatch-rose-violet { background: #8B5CF6; }
|
|
@@ -12172,7 +12219,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
12172
12219
|
color: var(--color-accent-primary);
|
|
12173
12220
|
}
|
|
12174
12221
|
.billing-cell-hit {
|
|
12175
|
-
color: #
|
|
12222
|
+
color: #2075ffbf;
|
|
12176
12223
|
}
|
|
12177
12224
|
.billing-cell-miss {
|
|
12178
12225
|
color: #f59e0b;
|
|
@@ -152,6 +152,11 @@ const BillingView = (() => {
|
|
|
152
152
|
</div>
|
|
153
153
|
</div>
|
|
154
154
|
|
|
155
|
+
<div class="billing-disclaimer">
|
|
156
|
+
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="1.4"/><path d="M8 7v4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><circle cx="8" cy="5" r="0.7" fill="currentColor"/></svg>
|
|
157
|
+
${I18n.t("billing.disclaimer")}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
155
160
|
<div class="billing-stats-row">
|
|
156
161
|
<div class="billing-stat-card">
|
|
157
162
|
<div class="billing-stat-icon billing-stat-icon-cost">
|
|
@@ -158,7 +158,11 @@ const ExtensionsStore = (() => {
|
|
|
158
158
|
try {
|
|
159
159
|
const res = await fetch("/api/store/extensions/brand");
|
|
160
160
|
const data = await res.json();
|
|
161
|
-
|
|
161
|
+
const all = data.extensions || [];
|
|
162
|
+
_extensions = _query
|
|
163
|
+
? all.filter(e => [e.name, e.name_zh, e.description].some(
|
|
164
|
+
f => f && f.toLowerCase().includes(_query.toLowerCase())))
|
|
165
|
+
: all;
|
|
162
166
|
_error = data.warning || null;
|
|
163
167
|
_loading = false;
|
|
164
168
|
_emit("extensions:changed", { extensions: _extensions, warning: _error });
|
|
@@ -181,7 +185,10 @@ const ExtensionsStore = (() => {
|
|
|
181
185
|
_detailError = null;
|
|
182
186
|
_emit("extensions:detail");
|
|
183
187
|
try {
|
|
184
|
-
const
|
|
188
|
+
const url = _filterBrand
|
|
189
|
+
? "/api/store/extension?id=" + encodeURIComponent(id) + "&source=brand"
|
|
190
|
+
: "/api/store/extension?id=" + encodeURIComponent(id);
|
|
191
|
+
const res = await fetch(url);
|
|
185
192
|
const data = await res.json();
|
|
186
193
|
if (res.ok && data.ok && data.extension) {
|
|
187
194
|
_detail = data.extension;
|