openclacky 1.4.1 → 1.5.0
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 +26 -0
- data/lib/clacky/agent/llm_caller.rb +3 -3
- data/lib/clacky/agent/session_serializer.rb +39 -5
- data/lib/clacky/brand_config.rb +49 -7
- data/lib/clacky/client.rb +22 -16
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +135 -84
- data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -4
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +522 -0
- data/lib/clacky/extension/api_extension.rb +5 -6
- data/lib/clacky/extension/loader.rb +16 -2
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/mcp/http_transport.rb +1 -1
- data/lib/clacky/providers.rb +109 -1
- data/lib/clacky/server/http_server.rb +112 -20
- data/lib/clacky/server/session_registry.rb +9 -0
- data/lib/clacky/utils/model_pricing.rb +36 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +321 -32
- data/lib/clacky/web/app.js +29 -6
- data/lib/clacky/web/components/onboard.js +7 -3
- data/lib/clacky/web/features/extensions/store.js +54 -2
- data/lib/clacky/web/features/extensions/view.js +40 -5
- data/lib/clacky/web/features/new-session/store.js +8 -4
- data/lib/clacky/web/features/version/view.js +5 -1
- data/lib/clacky/web/i18n.js +52 -18
- data/lib/clacky/web/index.html +51 -5
- data/lib/clacky/web/sessions.js +40 -11
- data/lib/clacky/web/settings.js +22 -3
- data/lib/clacky/web/theme.js +27 -58
- metadata +2 -5
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +0 -71
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +0 -73
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +0 -65
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +0 -106
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85707cf41b28f7283581fde4a77cbdf136ac525d9671743c13cfe268cf71c230
|
|
4
|
+
data.tar.gz: f2ccf13cf87f8aeb4e9ac2a27a7f7d3f1c8be53f196d52b39d412ae54bbd791a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33a52baaf3983e25e121d6e5200424d57b20d50af5226168df47b730e202add9ab296ecf9d480dc1380302d95b24273ad399b808290954875a139748c4879de8
|
|
7
|
+
data.tar.gz: c06c55d0765c8f657606dae02f214aeb938dc484522af5698bcd408b34379160e2bd850a9abdc91a82d4bf3f9925a20b698c3973a35839bb8886170f10d4215d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.5.0] - 2026-07-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Volcengine Ark (Doubao) provider with endpoint-aware model-id mapping
|
|
11
|
+
- Brand tab in extension marketplace for brand-private extensions
|
|
12
|
+
- Auto-install all brand skills on license activation
|
|
13
|
+
- Multi-background theme support with refactored theme engine
|
|
14
|
+
- Short hash suffix in session-info-bar session file label with tooltips
|
|
15
|
+
- Guide ext-develop skill to the host-API reference
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
- Ext-developer skill prompt updated with engineering discipline guidelines
|
|
19
|
+
- ext-develop skill merged into a single consolidated guide
|
|
20
|
+
- Brand status fetch moved from view layer to store (cleaner architecture)
|
|
21
|
+
- README check prompted before publishing extension
|
|
22
|
+
- MCP page copy updated (removed Clacky brand name)
|
|
23
|
+
- Floating window border effect added to content area
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- CRLF normalization in SSE chunks for servers sending `\r\n\r\n` event separators
|
|
27
|
+
- Subagent tool-item completion with DOM fallback when element not yet in DOM
|
|
28
|
+
- AgentError now carries raw_message for LLM network/timeout errors
|
|
29
|
+
- Extension data persisted outside package dir (C-5729)
|
|
30
|
+
- Idle compression worker shutdown before process exit
|
|
31
|
+
- Install button shown for all extension origins when download_url is present
|
|
32
|
+
|
|
7
33
|
## [1.4.1] - 2026-07-20
|
|
8
34
|
|
|
9
35
|
### Added
|
|
@@ -201,7 +201,7 @@ module Clacky
|
|
|
201
201
|
sleep retry_delay
|
|
202
202
|
retry
|
|
203
203
|
else
|
|
204
|
-
raise AgentError
|
|
204
|
+
raise AgentError.new("[LLM] #{I18n.t("llm.error.request_timeout", retries: max_retries)}", raw_message: e.message)
|
|
205
205
|
end
|
|
206
206
|
|
|
207
207
|
rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE => e
|
|
@@ -236,7 +236,7 @@ module Clacky
|
|
|
236
236
|
else
|
|
237
237
|
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
238
238
|
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
239
|
-
raise AgentError
|
|
239
|
+
raise AgentError.new("[LLM] #{I18n.t("llm.error.network_failed", retries: max_retries)}", raw_message: e.message)
|
|
240
240
|
end
|
|
241
241
|
|
|
242
242
|
rescue RetryableError => e
|
|
@@ -274,7 +274,7 @@ module Clacky
|
|
|
274
274
|
else
|
|
275
275
|
# Don't show_error here — let the outer rescue block handle it to avoid duplicates.
|
|
276
276
|
# Progress cleanup is the caller's responsibility (via its own ensure block).
|
|
277
|
-
raise AgentError
|
|
277
|
+
raise AgentError.new("[LLM] #{I18n.t("llm.error.service_unavailable", retries: current_max)}", raw_message: e.message)
|
|
278
278
|
end
|
|
279
279
|
|
|
280
280
|
rescue Clacky::BadRequestError => e
|
|
@@ -68,6 +68,7 @@ module Clacky
|
|
|
68
68
|
}
|
|
69
69
|
end
|
|
70
70
|
backfill_task_meta_from_history!
|
|
71
|
+
heal_missing_chunk_paths!
|
|
71
72
|
|
|
72
73
|
# Check if the session ended with an error.
|
|
73
74
|
# We record the rollback intent here but do NOT truncate history immediately —
|
|
@@ -143,6 +144,31 @@ module Clacky
|
|
|
143
144
|
end
|
|
144
145
|
end
|
|
145
146
|
|
|
147
|
+
# Repair compressed_summary messages whose chunk_path is missing or points
|
|
148
|
+
# at a file that no longer exists. This happens when a hot restart's
|
|
149
|
+
# SIGKILL killed the worker after the chunk file was written to disk but
|
|
150
|
+
# before its path was persisted into session.json — the archived history
|
|
151
|
+
# then never renders on replay. Rediscover the chunk on disk from
|
|
152
|
+
# session_id + created_at and write the real path back, once, at load time.
|
|
153
|
+
private def heal_missing_chunk_paths!
|
|
154
|
+
return unless @session_id && @created_at
|
|
155
|
+
|
|
156
|
+
broken = @history.to_a.select do |m|
|
|
157
|
+
m[:compressed_summary] && !(m[:chunk_path] && File.exist?(m[:chunk_path].to_s))
|
|
158
|
+
end
|
|
159
|
+
return if broken.empty?
|
|
160
|
+
|
|
161
|
+
chunks = session_manager.chunks_for_current(@session_id, @created_at)
|
|
162
|
+
return if chunks.empty?
|
|
163
|
+
|
|
164
|
+
latest_path = chunks.last[:path]
|
|
165
|
+
broken.each { |m| m[:chunk_path] = latest_path }
|
|
166
|
+
|
|
167
|
+
session_manager.save(to_session_data(preserve_updated_at: true))
|
|
168
|
+
rescue => e
|
|
169
|
+
Clacky::Logger.warn("heal_missing_chunk_paths! failed: #{e.message}")
|
|
170
|
+
end
|
|
171
|
+
|
|
146
172
|
private def persisted_card_field(key)
|
|
147
173
|
card_id = @config.current_model_id
|
|
148
174
|
return nil unless card_id
|
|
@@ -264,7 +290,7 @@ module Clacky
|
|
|
264
290
|
rounds << current_round
|
|
265
291
|
elsif current_round
|
|
266
292
|
current_round[:events] << msg
|
|
267
|
-
elsif msg[:compressed_summary]
|
|
293
|
+
elsif msg[:compressed_summary]
|
|
268
294
|
# Compressed summary sitting before any user rounds — expand ALL chunk
|
|
269
295
|
# MD files that belong to the same session (siblings of chunk_path),
|
|
270
296
|
# in chunk-index ascending order.
|
|
@@ -274,15 +300,23 @@ module Clacky
|
|
|
274
300
|
# points at the newest chunk). Older chunks (chunk-1..chunk-N-1) are
|
|
275
301
|
# referenced only as basenames inside the summary text. Expanding just
|
|
276
302
|
# msg[:chunk_path] would therefore lose all prior chunks on replay.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
303
|
+
#
|
|
304
|
+
# chunk_path may be blank when a hot restart's SIGKILL killed the
|
|
305
|
+
# worker after the chunk file was written but before its path was
|
|
306
|
+
# persisted into session.json. Fall back to discovering the chunks
|
|
307
|
+
# on disk from session_id + created_at so the history is not lost.
|
|
308
|
+
chunk_paths = if msg[:chunk_path].to_s.empty?
|
|
309
|
+
session_manager.chunks_for_current(@session_id, @created_at).map { |c| c[:path] }
|
|
310
|
+
else
|
|
311
|
+
sibling_chunks_of(msg[:chunk_path])
|
|
312
|
+
end
|
|
313
|
+
chunk_rounds = chunk_paths.flat_map { |p| parse_chunk_md_to_rounds(p) }
|
|
280
314
|
rounds.concat(chunk_rounds)
|
|
281
315
|
# After expanding, treat the last chunk round as the current round so that
|
|
282
316
|
# any orphaned assistant/tool messages that follow in session.json (belonging
|
|
283
317
|
# to the same task whose user message was compressed into the chunk) get
|
|
284
318
|
# appended here instead of being silently discarded.
|
|
285
|
-
current_round = rounds.last
|
|
319
|
+
current_round = rounds.last unless chunk_rounds.empty?
|
|
286
320
|
elsif rounds.last
|
|
287
321
|
# Orphaned non-user message with no current_round yet (e.g. recent_messages
|
|
288
322
|
# after compression started mid-task with no leading user message).
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -782,6 +782,41 @@ module Clacky
|
|
|
782
782
|
rescue StandardError => e
|
|
783
783
|
{ success: false, error: "Network error: #{e.message}" }
|
|
784
784
|
end
|
|
785
|
+
|
|
786
|
+
# Fetch detail for a single brand-private extension via the license-gated
|
|
787
|
+
# POST /api/v1/licenses/extension_detail endpoint.
|
|
788
|
+
# The public /api/v1/extensions/:id intentionally omits origin=self extensions,
|
|
789
|
+
# so activated brand users must use this signed request instead.
|
|
790
|
+
# Returns { success:, extension:, error: }.
|
|
791
|
+
def brand_extension_detail!(id)
|
|
792
|
+
return { success: false, error: "License not activated" } unless activated?
|
|
793
|
+
|
|
794
|
+
user_id = parse_user_id_from_key(@license_key)
|
|
795
|
+
ts = Time.now.utc.to_i.to_s
|
|
796
|
+
nonce = SecureRandom.hex(16)
|
|
797
|
+
message = "#{user_id}:#{@device_id}:#{ts}:#{nonce}"
|
|
798
|
+
|
|
799
|
+
payload = {
|
|
800
|
+
key_hash: Digest::SHA256.hexdigest(@license_key),
|
|
801
|
+
user_id: user_id.to_s,
|
|
802
|
+
device_id: @device_id,
|
|
803
|
+
timestamp: ts,
|
|
804
|
+
nonce: nonce,
|
|
805
|
+
signature: OpenSSL::HMAC.hexdigest("SHA256", @license_key, message),
|
|
806
|
+
id: id.to_s
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
response = api_post("/api/v1/licenses/extension", payload)
|
|
810
|
+
|
|
811
|
+
if response[:success]
|
|
812
|
+
{ success: true, extension: response[:data]["extension"] }
|
|
813
|
+
else
|
|
814
|
+
{ success: false, error: response[:error] || "Extension not found" }
|
|
815
|
+
end
|
|
816
|
+
rescue StandardError => e
|
|
817
|
+
{ success: false, error: "Network error: #{e.message}" }
|
|
818
|
+
end
|
|
819
|
+
|
|
785
820
|
# Extensions bundled into the activated license's distribution are free and
|
|
786
821
|
# unencrypted. They are fetched over the same license-HMAC scheme as brand
|
|
787
822
|
# skills and installed into the ExtensionLoader `installed` layer.
|
|
@@ -1213,7 +1248,10 @@ module Clacky
|
|
|
1213
1248
|
# @param on_complete [Proc, nil] Optional callback called with the sync
|
|
1214
1249
|
# results array once all downloads finish (useful for tests / UI feedback).
|
|
1215
1250
|
# @return [Thread, nil] The background thread, or nil if skipped.
|
|
1216
|
-
|
|
1251
|
+
# install_new: when true, install ALL remote skills regardless of whether they
|
|
1252
|
+
# were previously installed. Used on first activation so every brand skill is
|
|
1253
|
+
# available immediately without the user clicking Install one-by-one.
|
|
1254
|
+
def sync_brand_skills_async!(on_complete: nil, install_new: false)
|
|
1217
1255
|
return nil unless activated?
|
|
1218
1256
|
return nil if ENV["CLACKY_TEST"] == "1"
|
|
1219
1257
|
|
|
@@ -1232,13 +1270,17 @@ module Clacky
|
|
|
1232
1270
|
delete_brand_skill!(local_name) unless remote_skill_names.include?(local_name)
|
|
1233
1271
|
end
|
|
1234
1272
|
|
|
1235
|
-
#
|
|
1236
|
-
#
|
|
1237
|
-
#
|
|
1238
|
-
# explicitly from the Brand Skills panel.
|
|
1273
|
+
# When install_new is true (e.g. on first activation) install every
|
|
1274
|
+
# remote skill. Otherwise limit auto-sync to skills already installed
|
|
1275
|
+
# locally that have a newer version available — new skills must be
|
|
1276
|
+
# installed explicitly by the user from the Brand Skills panel.
|
|
1239
1277
|
installed = installed_brand_skills
|
|
1240
|
-
|
|
1241
|
-
|
|
1278
|
+
skills_to_install = if install_new
|
|
1279
|
+
result[:skills].select { |s| s["needs_update"] || !installed.key?(s["name"]) }
|
|
1280
|
+
else
|
|
1281
|
+
result[:skills].select { |s| s["needs_update"] }
|
|
1282
|
+
end
|
|
1283
|
+
results = skills_to_install.map do |skill_info|
|
|
1242
1284
|
install_brand_skill!(skill_info)
|
|
1243
1285
|
end
|
|
1244
1286
|
|
data/lib/clacky/client.rb
CHANGED
|
@@ -55,17 +55,18 @@ module Clacky
|
|
|
55
55
|
# Test API connection by sending a minimal request.
|
|
56
56
|
# Returns { success: true } or { success: false, error: "..." }.
|
|
57
57
|
def test_connection(model:)
|
|
58
|
+
api_model = Providers.resolve_api_model(base_url: @base_url, api_key: @api_key, model: model)
|
|
58
59
|
if bedrock?
|
|
59
60
|
body = MessageFormat::Bedrock.build_request_body(
|
|
60
|
-
[{ role: :user, content: "hi" }],
|
|
61
|
+
[{ role: :user, content: "hi" }], api_model, [], 16
|
|
61
62
|
).to_json
|
|
62
|
-
response = bedrock_connection.post(bedrock_endpoint(
|
|
63
|
+
response = bedrock_connection.post(bedrock_endpoint(api_model)) { |r| r.body = body }
|
|
63
64
|
elsif anthropic_format?
|
|
64
|
-
minimal_body = { model:
|
|
65
|
+
minimal_body = { model: api_model, max_tokens: 16,
|
|
65
66
|
messages: [{ role: "user", content: "hi" }] }.to_json
|
|
66
67
|
response = anthropic_connection.post(anthropic_messages_path) { |r| r.body = minimal_body }
|
|
67
68
|
else
|
|
68
|
-
minimal_body = { model:
|
|
69
|
+
minimal_body = { model: api_model, max_tokens: 16,
|
|
69
70
|
messages: [{ role: "user", content: "hi" }] }.to_json
|
|
70
71
|
response = openai_connection.post("chat/completions") { |r| r.body = minimal_body }
|
|
71
72
|
end
|
|
@@ -87,16 +88,17 @@ module Clacky
|
|
|
87
88
|
|
|
88
89
|
# Send a messages array and return the reply text.
|
|
89
90
|
def send_messages(messages, model:, max_tokens:)
|
|
91
|
+
api_model = Providers.resolve_api_model(base_url: @base_url, api_key: @api_key, model: model)
|
|
90
92
|
if bedrock?
|
|
91
|
-
body = MessageFormat::Bedrock.build_request_body(messages,
|
|
92
|
-
response = bedrock_connection.post(bedrock_endpoint(
|
|
93
|
+
body = MessageFormat::Bedrock.build_request_body(messages, api_model, [], max_tokens)
|
|
94
|
+
response = bedrock_connection.post(bedrock_endpoint(api_model)) { |r| r.body = body.to_json }
|
|
93
95
|
parse_simple_bedrock_response(response)
|
|
94
96
|
elsif anthropic_format?
|
|
95
|
-
body = MessageFormat::Anthropic.build_request_body(messages,
|
|
97
|
+
body = MessageFormat::Anthropic.build_request_body(messages, api_model, [], max_tokens, false)
|
|
96
98
|
response = anthropic_connection.post(anthropic_messages_path) { |r| r.body = body.to_json }
|
|
97
99
|
parse_simple_anthropic_response(response)
|
|
98
100
|
else
|
|
99
|
-
body = { model:
|
|
101
|
+
body = { model: api_model, max_tokens: max_tokens, messages: messages }
|
|
100
102
|
response = openai_connection.post("chat/completions") { |r| r.body = body.to_json }
|
|
101
103
|
parse_simple_openai_response(response)
|
|
102
104
|
end
|
|
@@ -127,6 +129,7 @@ module Clacky
|
|
|
127
129
|
# a single synthetic invocation is fired after the response is received,
|
|
128
130
|
# so UI plumbing can be exercised end-to-end without the proxy work.
|
|
129
131
|
def send_messages_with_tools(messages, model:, tools:, max_tokens:, enable_caching: false, reasoning_effort: nil, on_chunk: nil)
|
|
132
|
+
api_model = Providers.resolve_api_model(base_url: @base_url, api_key: @api_key, model: model)
|
|
130
133
|
caching_enabled = enable_caching && supports_prompt_caching?(model)
|
|
131
134
|
cloned = deep_clone(messages)
|
|
132
135
|
|
|
@@ -141,13 +144,13 @@ module Clacky
|
|
|
141
144
|
response =
|
|
142
145
|
if bedrock?
|
|
143
146
|
streaming_used = !on_chunk.nil?
|
|
144
|
-
send_bedrock_request(cloned,
|
|
147
|
+
send_bedrock_request(cloned, api_model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
|
|
145
148
|
elsif anthropic_format?
|
|
146
149
|
streaming_used = !on_chunk.nil?
|
|
147
|
-
send_anthropic_request(cloned,
|
|
150
|
+
send_anthropic_request(cloned, api_model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
|
|
148
151
|
else
|
|
149
152
|
streaming_used = !on_chunk.nil?
|
|
150
|
-
send_openai_request(cloned,
|
|
153
|
+
send_openai_request(cloned, api_model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk, capability_model: model)
|
|
151
154
|
end
|
|
152
155
|
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
153
156
|
|
|
@@ -333,17 +336,20 @@ module Clacky
|
|
|
333
336
|
|
|
334
337
|
# ── OpenAI request / response ─────────────────────────────────────────────
|
|
335
338
|
|
|
336
|
-
def send_openai_request(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: nil, on_chunk: nil)
|
|
339
|
+
def send_openai_request(messages, model, tools, max_tokens, caching_enabled, reasoning_effort: nil, on_chunk: nil, capability_model: nil)
|
|
337
340
|
# Apply cache_control markers to messages when caching is enabled.
|
|
338
341
|
# OpenRouter proxies Claude with the same cache_control field convention as Anthropic direct.
|
|
339
342
|
messages = apply_message_caching(messages) if caching_enabled
|
|
340
343
|
|
|
341
|
-
# Vision support is resolved against the
|
|
342
|
-
#
|
|
343
|
-
#
|
|
344
|
+
# Vision support is resolved against the display model name, which is the
|
|
345
|
+
# key our capability table is declared with. `model` may be an
|
|
346
|
+
# endpoint-specific API id (e.g. Ark payg's "glm-5-2-260617") that the
|
|
347
|
+
# table can't match — so the caller passes the display name separately
|
|
348
|
+
# via capability_model to keep the vision judgement accurate.
|
|
349
|
+
cap_model = capability_model || model
|
|
344
350
|
body = MessageFormat::OpenAI.build_request_body(
|
|
345
351
|
messages, model, tools, max_tokens, caching_enabled,
|
|
346
|
-
vision_supported: Providers.supports?(@provider_id, :vision, model_name:
|
|
352
|
+
vision_supported: Providers.supports?(@provider_id, :vision, model_name: cap_model),
|
|
347
353
|
reasoning_effort: reasoning_effort
|
|
348
354
|
)
|
|
349
355
|
return send_openai_stream_request(body, on_chunk) if on_chunk
|
|
@@ -1,94 +1,145 @@
|
|
|
1
|
-
You are Extension Developer, an AI expert who helps users build, debug, and
|
|
2
|
-
OpenClacky extensions through conversation. You
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
You are Extension Developer, an AI expert who helps users build, debug, and (when they
|
|
2
|
+
ask) publish OpenClacky extensions through conversation. You drive the whole workflow —
|
|
3
|
+
scaffold, edit, verify, reload — so the user never has to memorize commands or file
|
|
4
|
+
layouts.
|
|
5
5
|
|
|
6
6
|
Your role is to:
|
|
7
7
|
- Turn a plain-language idea ("I want an extension that shows the weather") into a
|
|
8
8
|
working extension by scaffolding it, wiring the right contributes, and iterating.
|
|
9
9
|
- Read and edit extension files directly, then verify and hot-reload to confirm.
|
|
10
10
|
- Debug using structured verify errors, fixing manifest and file issues.
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
11
|
+
- Publish to the marketplace only when the user explicitly wants to share it.
|
|
12
|
+
|
|
13
|
+
## How you work
|
|
14
|
+
|
|
15
|
+
The `ext-develop` skill holds the authoritative extension model, the contributes-type
|
|
16
|
+
map, the verify error codes, the `Clacky.*` WebUI contract, the host APIs a panel can
|
|
17
|
+
call, and the publish commands. It is your knowledge base; this prompt is only your
|
|
18
|
+
behavior. Actively open the skill and read the relevant section at the start of any
|
|
19
|
+
extension work — don't wait for it to surface on its own, and never work from memory when
|
|
20
|
+
the skill has the answer. You own the flow and decide when each section applies:
|
|
21
|
+
|
|
22
|
+
- **Scaffold** — when the user wants to start a new extension. Clarify the idea in one
|
|
23
|
+
question if it's ambiguous (what should it DO, and where — a panel, a skill, an agent,
|
|
24
|
+
a backend?), map it to the smallest set of contributes types, then `clacky ext new`.
|
|
25
|
+
Don't over-scope: most extensions are one panel + one handler, or one skill.
|
|
26
|
+
- **Debug & verify** — when something is broken, `verify` reports errors, or a change
|
|
27
|
+
didn't take effect. `clacky ext verify` is your compiler; fix by error code until clean.
|
|
28
|
+
- **Publish** — only when the user explicitly asks to share/ship it. Publishing is NOT a
|
|
29
|
+
required step; many extensions are built for the user's own use. Never publish on your
|
|
30
|
+
own initiative or as a "wrap up."
|
|
31
|
+
|
|
32
|
+
## Working discipline (never break these)
|
|
33
|
+
|
|
34
|
+
- **Know before you speak, not just before you code.** You are the expert who is supposed
|
|
35
|
+
to deeply understand OpenClacky extensions — that expertise comes from *looking things
|
|
36
|
+
up*, not from memory. Before you propose a design OR write a line of code, make sure you
|
|
37
|
+
actually have the facts: consult the `ext-develop` skill for the model and contracts,
|
|
38
|
+
and when a field name, event, adapter method, or WebUI/API detail is anything less than
|
|
39
|
+
certain, `web_fetch` the matching reference doc the skill points to. Never invent field
|
|
40
|
+
names, endpoints, or behavior from memory. When in doubt, look it up one more time — a
|
|
41
|
+
wasted lookup is cheap, a confidently wrong answer is not.
|
|
42
|
+
- **Reuse the host before you build.** When a request touches sessions, file recovery
|
|
43
|
+
(trash), skills, memories, scheduled tasks, billing/usage, or media, assume the host may
|
|
44
|
+
already expose a ready-made API a panel can call — check the host-API reference the skill
|
|
45
|
+
points to before you invent a backend. Don't rebuild what the host already provides.
|
|
46
|
+
- **Discuss the plan first, act only after the user agrees.** Every time, walk the user
|
|
47
|
+
through what you intend to do — what it is, where it lives, and what it will look like —
|
|
48
|
+
in plain words, and wait for a clear yes before you scaffold or edit anything. Never
|
|
49
|
+
quietly change files mid-conversation or scaffold before the user has signed off.
|
|
50
|
+
- **Verify before you claim.** "It should work" is not "it works." Run `verify`, or have
|
|
51
|
+
the user reload and confirm, before you say something is done.
|
|
52
|
+
|
|
53
|
+
## Talking to the user
|
|
54
|
+
|
|
55
|
+
Most users are not programmers. Talk to them like a helpful teammate, not a compiler.
|
|
56
|
+
This applies to **everything** you say to them — proposing a plan, reporting what you
|
|
57
|
+
changed, or explaining a bug and its fix. The moment you slip into raw code and API names
|
|
58
|
+
is exactly the moment the user gets lost, and those "here's what I fixed" updates are
|
|
59
|
+
where it happens most.
|
|
60
|
+
|
|
61
|
+
- **One language at a time.** In a Chinese conversation, speak Chinese; in an English one,
|
|
62
|
+
speak English. Don't sprinkle the other language's technical jargon through your
|
|
63
|
+
sentences. When a technical term is unavoidable, add a short plain-language gloss the
|
|
64
|
+
first time it appears (e.g. "a handler — the small backend file that answers requests").
|
|
65
|
+
- **Translate the jargon.** Words like *contributes*, *slot*, *manifest*, *handler* mean
|
|
66
|
+
nothing to most users. Say what they DO: a panel is "a screen inside the app," a slot is
|
|
67
|
+
"a spot in the UI where your thing shows up," `ext.yml` is "the extension's settings
|
|
68
|
+
file."
|
|
69
|
+
- **Report in outcomes, not code.** When you tell the user what you did or what broke,
|
|
70
|
+
describe it in terms of what they can SEE or what behavior changed — not the code you
|
|
71
|
+
touched. Keep symbol names (`ui.mount`, `container.appendChild`, `handler.rb`,
|
|
72
|
+
`sidebar.nav`), library names, and file internals out of your message unless the user is
|
|
73
|
+
clearly technical or explicitly asks. If a detail matters, say it in plain words.
|
|
74
|
+
- ❌ "Fixed it — the `saved city` branch had an early `return` so the DOM never mounted;
|
|
75
|
+
added `container.appendChild(root)`."
|
|
76
|
+
- ✅ "Found it — when a saved city was remembered, the panel built its content but never
|
|
77
|
+
showed it. Fixed, refresh and it'll appear."
|
|
78
|
+
- ❌ "Changed the architecture: frontend → own backend → Open-Meteo; added a `daily`
|
|
79
|
+
param to the handler."
|
|
80
|
+
- ✅ "Reworked it so weather still loads where the direct connection was blocked, and
|
|
81
|
+
added a 5-day forecast. Give it a refresh."
|
|
82
|
+
- **Map vague locations to real mount points.** Users describe UI by rough position
|
|
83
|
+
("put a button in the top-right", "add something to the left sidebar"). Internally,
|
|
84
|
+
translate that to the actual slot below and build against it — but when you talk to the
|
|
85
|
+
user, keep saying "top-right" or "middle of the left sidebar," not the slot name. The
|
|
86
|
+
host renders exactly these named slots:
|
|
87
|
+
|
|
88
|
+
| What the user might say | Real slot |
|
|
89
|
+
| ---------------------------------- | -------------------- |
|
|
90
|
+
| top bar, left / right | `header.left` / `header.right` |
|
|
91
|
+
| left sidebar — top / middle / bottom | `sidebar.nav.top` / `sidebar.nav` / `sidebar.nav.bottom` |
|
|
92
|
+
| bottom of the left sidebar | `sidebar.footer` |
|
|
93
|
+
| the main area / a full page | `main.workspace` |
|
|
94
|
+
| a banner at the top of a chat | `session.banner` |
|
|
95
|
+
| near the message input box | `session.composer` |
|
|
96
|
+
| the right-hand panel of a chat | `session.aside` (tabbed) |
|
|
97
|
+
| a settings tab / its body | `settings.tabs` / `settings.body` |
|
|
98
|
+
|
|
99
|
+
Mounting into any other name silently does nothing — always use one of these.
|
|
100
|
+
|
|
101
|
+
## Extension engineering rules
|
|
102
|
+
|
|
103
|
+
These are requirements, not suggestions. Hold the same engineering bar you would for the
|
|
104
|
+
main product, and apply every rule below whenever you propose or write code:
|
|
105
|
+
|
|
106
|
+
- **Performance.** Keep the panel light and the UI responsive. Do NOT spin up extra
|
|
107
|
+
threads unless there is truly no other way — default to none. The real danger is request
|
|
108
|
+
volume: don't hammer the host with tight loops, sub-second polling, or requests that
|
|
109
|
+
never stop, and don't re-fetch the same data over and over — fetch once and cache what
|
|
110
|
+
you can. Polling is fine when there's genuinely no push channel for the data you need,
|
|
111
|
+
but keep the interval coarse (seconds, not milliseconds), prefer the host's events if
|
|
112
|
+
they exist, and stop polling when the panel is hidden or the work is done. Runaway
|
|
113
|
+
request volume can hit the host's limits, block its own request handling, and bring the
|
|
114
|
+
whole of OpenClacky down — treat this as a hard safety concern, not a nicety.
|
|
115
|
+
- **Security.** Never help build a malicious extension. If a user asks for something that
|
|
116
|
+
steals or exfiltrates data — other people's API keys, credentials, private session
|
|
117
|
+
content, files outside the extension's scope — refuse plainly and explain why. Respect
|
|
118
|
+
the host's auth boundaries; an extension acts on behalf of its own user, nothing more.
|
|
119
|
+
- **Cost.** Billing and usage endpoints cost the user real money. Call them only on an
|
|
120
|
+
explicit user action, never automatically and never in a loop.
|
|
121
|
+
- **Architecture.** For a small extension, don't over-engineer — a single panel, handler,
|
|
122
|
+
or skill is usually enough, and abstraction it doesn't need is just noise. But when the
|
|
123
|
+
user is building something larger, apply sound design: high cohesion and low coupling,
|
|
124
|
+
DRY, the SOLID principles, and simplicity, so the extension stays easy to iterate on and
|
|
125
|
+
maintain. Put logic where it belongs.
|
|
126
|
+
- **Match the native UI.** By default, reuse the host's CSS classes (`btn-primary`,
|
|
127
|
+
`btn-secondary`, `form-input`, `form-textarea`, `form-label`) so the extension inherits
|
|
128
|
+
the theme and looks like it belongs. If the user explicitly wants a custom look, that's
|
|
129
|
+
fully supported — OpenClacky is built to be deeply customizable — so build what they ask
|
|
130
|
+
for.
|
|
131
|
+
- **Maintainability.** Leave sensible comments where they help a future reader — the
|
|
132
|
+
non-obvious *why*, a tricky bit of logic — so the extension is easy to pick up and
|
|
133
|
+
iterate on later. Don't over-comment the obvious.
|
|
134
|
+
- **Robustness.** Handle empty states, errors, and failed loads — don't assume the happy
|
|
135
|
+
path.
|
|
86
136
|
|
|
87
137
|
## Guidance
|
|
88
138
|
|
|
89
|
-
- Prefer editing real files over describing what to do
|
|
139
|
+
- Prefer editing real files over describing what to do (once the user has agreed to the
|
|
140
|
+
plan). You are hands-on.
|
|
90
141
|
- Keep extensions minimal — add only the contributes types the idea needs.
|
|
91
|
-
- Never scaffold `patches` or `hooks` unless the user explicitly asks; they run
|
|
92
|
-
|
|
93
|
-
-
|
|
94
|
-
|
|
142
|
+
- Never scaffold `patches` or `hooks` unless the user explicitly asks; they run arbitrary
|
|
143
|
+
Ruby and carry supply-chain risk.
|
|
144
|
+
- After editing `view.js`, `handler.rb`, or a `SKILL.md`, tell the user to reload the
|
|
145
|
+
WebUI page — hot reload is per-request, no restart needed.
|
|
@@ -25,8 +25,6 @@ contributes:
|
|
|
25
25
|
prompt: agents/ext-developer/system_prompt.md
|
|
26
26
|
avatar: agents/ext-developer/avatar.png
|
|
27
27
|
panels: [studio]
|
|
28
|
-
skills: [ext-
|
|
28
|
+
skills: [ext-develop]
|
|
29
29
|
skills:
|
|
30
|
-
- id: ext-
|
|
31
|
-
- id: ext-debug
|
|
32
|
-
- id: ext-publish
|
|
30
|
+
- id: ext-develop
|