openclacky 1.4.1 → 1.5.1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/lib/clacky/agent/llm_caller.rb +3 -3
  4. data/lib/clacky/agent/session_serializer.rb +39 -5
  5. data/lib/clacky/brand_config.rb +49 -7
  6. data/lib/clacky/client.rb +22 -16
  7. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +135 -84
  8. data/lib/clacky/default_extensions/ext-studio/ext.yml +2 -4
  9. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +522 -0
  10. data/lib/clacky/extension/api_extension.rb +5 -6
  11. data/lib/clacky/extension/loader.rb +16 -2
  12. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  13. data/lib/clacky/mcp/http_transport.rb +1 -1
  14. data/lib/clacky/providers.rb +109 -1
  15. data/lib/clacky/server/http_server.rb +115 -19
  16. data/lib/clacky/server/session_registry.rb +9 -0
  17. data/lib/clacky/utils/model_pricing.rb +36 -0
  18. data/lib/clacky/version.rb +1 -1
  19. data/lib/clacky/web/app.css +321 -32
  20. data/lib/clacky/web/app.js +29 -6
  21. data/lib/clacky/web/components/onboard.js +7 -3
  22. data/lib/clacky/web/features/extensions/store.js +54 -2
  23. data/lib/clacky/web/features/extensions/view.js +40 -5
  24. data/lib/clacky/web/features/new-session/store.js +8 -4
  25. data/lib/clacky/web/features/version/view.js +5 -1
  26. data/lib/clacky/web/i18n.js +52 -18
  27. data/lib/clacky/web/index.html +51 -5
  28. data/lib/clacky/web/sessions.js +40 -11
  29. data/lib/clacky/web/settings.js +22 -3
  30. data/lib/clacky/web/theme.js +27 -58
  31. data/scripts/build/src/uninstall.sh.cc +1 -1
  32. metadata +2 -5
  33. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +0 -71
  34. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +0 -73
  35. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +0 -65
  36. 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: 4c8311a2bd1bd4cd050fce3e806479c580afb4da476201e59a041c2293347b5f
4
- data.tar.gz: 520ba1c8c1821aede0b4766be3f321b3c121a91d3d9fe9e5dada0034479a3d4e
3
+ metadata.gz: b195de6fb8b38aed47ef40aa59fa0e33aa3b993f52cc584d5c12bd27bd15b55c
4
+ data.tar.gz: 25b4a406f7f4985e4bdad2cd63c28c2af8875bed7ddf59baae33a94d9ebbe46b
5
5
  SHA512:
6
- metadata.gz: e2d171e71ae9eb9cbecfc79bbf6ac1f3977268baf5a4026a57caffc378501ca6ca1bad8426237bcd949564ac1986d1b5b4bfe4f549f108d35f3e216edee75bdc
7
- data.tar.gz: 8ba4d9fe3a50aacd4a247aae6877f391c73297c4e63b8c428b99a09f9c2d9ab642104ff5d6370c04556b679b2e3d041785f1833727605b3205638e7c7ea01126
6
+ metadata.gz: 15a21c8d7aa23bbccc4a8255cb2caa14a937ff92ec90cbf20fac58a71675acfdd1f64be47723151841f860ed069add58e43abdccb48b31233cde8351ab9fad23
7
+ data.tar.gz: 3fbaadab5fcf374369ece22818f2739839ebf974d91a0ecf1dba9aa9e0530e27a801e4366ab2f6403c222501601e4e76f736d73d81eb667ee275e150364d21a1
data/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ 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.1] - 2026-07-24
8
+
9
+ ### Fixed
10
+ - Extension marketplace detail page now opens correctly for brand users when clicking public extensions
11
+ - Uninstall script now properly removes all installed openclacky gem versions
12
+
13
+ ## [1.5.0] - 2026-07-23
14
+
15
+ ### Added
16
+ - Volcengine Ark (Doubao) provider with endpoint-aware model-id mapping
17
+ - Brand tab in extension marketplace for brand-private extensions
18
+ - Auto-install all brand skills on license activation
19
+ - Multi-background theme support with refactored theme engine
20
+ - Short hash suffix in session-info-bar session file label with tooltips
21
+ - Guide ext-develop skill to the host-API reference
22
+
23
+ ### Improved
24
+ - Ext-developer skill prompt updated with engineering discipline guidelines
25
+ - ext-develop skill merged into a single consolidated guide
26
+ - Brand status fetch moved from view layer to store (cleaner architecture)
27
+ - README check prompted before publishing extension
28
+ - MCP page copy updated (removed Clacky brand name)
29
+ - Floating window border effect added to content area
30
+
31
+ ### Fixed
32
+ - CRLF normalization in SSE chunks for servers sending `\r\n\r\n` event separators
33
+ - Subagent tool-item completion with DOM fallback when element not yet in DOM
34
+ - AgentError now carries raw_message for LLM network/timeout errors
35
+ - Extension data persisted outside package dir (C-5729)
36
+ - Idle compression worker shutdown before process exit
37
+ - Install button shown for all extension origins when download_url is present
38
+
7
39
  ## [1.4.1] - 2026-07-20
8
40
 
9
41
  ### Added
@@ -201,7 +201,7 @@ module Clacky
201
201
  sleep retry_delay
202
202
  retry
203
203
  else
204
- raise AgentError, "[LLM] #{I18n.t("llm.error.request_timeout", retries: max_retries)}"
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, "[LLM] #{I18n.t("llm.error.network_failed", retries: max_retries)}"
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, "[LLM] #{I18n.t("llm.error.service_unavailable", retries: current_max)}"
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] && msg[:chunk_path]
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
- chunk_rounds = sibling_chunks_of(msg[:chunk_path]).flat_map { |p|
278
- parse_chunk_md_to_rounds(p)
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).
@@ -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
- def sync_brand_skills_async!(on_complete: nil)
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
- # Auto-sync is intentionally limited to skills the user has already
1236
- # installed and that have a newer version available.
1237
- # New skills are never auto-installedthe user must click Install/Update
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
- skills_needing_update = result[:skills].select { |s| s["needs_update"] }
1241
- results = skills_needing_update.map do |skill_info|
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" }], model, [], 16
61
+ [{ role: :user, content: "hi" }], api_model, [], 16
61
62
  ).to_json
62
- response = bedrock_connection.post(bedrock_endpoint(model)) { |r| r.body = body }
63
+ response = bedrock_connection.post(bedrock_endpoint(api_model)) { |r| r.body = body }
63
64
  elsif anthropic_format?
64
- minimal_body = { model: model, max_tokens: 16,
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: model, max_tokens: 16,
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, model, [], max_tokens)
92
- response = bedrock_connection.post(bedrock_endpoint(model)) { |r| r.body = body.to_json }
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, model, [], max_tokens, false)
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: model, max_tokens: max_tokens, messages: messages }
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, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
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, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
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, model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
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 request's actual model (which may
342
- # differ from @model after a runtime switch or fallback override), so the
343
- # conversion layer strips image_url blocks for non-vision models.
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: model),
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 publish
2
- OpenClacky extensions through conversation. You know the OpenClacky extension system
3
- inside out and drive the whole workflow scaffold, edit, verify, reload, publish —
4
- so the user never has to memorize commands or file layouts.
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
- - Guide the user through packing and publishing to the marketplace.
12
-
13
- ## The Extension Model (ground truth)
14
-
15
- An extension is one directory containing an `ext.yml` manifest that declares
16
- `contributes:`. Nothing is nested units reference each other by id.
17
-
18
- Three layers, override precedence `local > installed > builtin`:
19
- - `builtin` bundled in the gem (`default_extensions/`)
20
- - `installed` `~/.clacky/ext/installed/<id>/` (from `ext install`)
21
- - `local` — `~/.clacky/ext/local/<id>/` (where users develop; `ext new` lands here)
22
-
23
- Seven contributes types (each is a self-describing unit):
24
- - `panels` — WebUI panels (a `view.js`, no build step, no React, no iframe)
25
- - `api` — one backend file `api/handler.rb`, mounted at `/api/ext/<id>/`
26
- - `skills` a `SKILL.md` under `skills/<id>/` (prompt-only capability)
27
- - `agents` — a `system_prompt.md`; can reference `panels: [id]` and `skills: [id]`
28
- - `channels`an IM adapter
29
- - `patches` monkey-patch a real class (advanced, supply-chain risk)
30
- - `hooks` — lifecycle hooks like `before_tool_use` (advanced)
31
-
32
- Hot reload is per-request: after editing `view.js`, `handler.rb`, or a `SKILL.md`,
33
- the user just reloads the WebUI page — no server restart. Editing `ext.yml` also
34
- takes effect on the next load.
35
-
36
- ## ext.yml shape (memorize this)
37
-
38
- ```yaml
39
- id: my-ext
40
- name: My Extension
41
- description: what it does
42
- version: "0.1.0"
43
- contributes:
44
- api: api/handler.rb
45
- panels:
46
- - id: dashboard
47
- view: panels/dashboard/view.js
48
- attach: ["*"] # panels/agents to attach to; "*" = all
49
- skills:
50
- - id: my-ext-skill # SKILL.md lives at skills/my-ext-skill/SKILL.md
51
- agents:
52
- - id: helper
53
- title: Helper
54
- prompt: agents/helper/system_prompt.md
55
- panels: [dashboard]
56
- skills: [my-ext-skill]
57
- ```
58
-
59
- Panel `view.js` mounts via `Clacky.ext.ui.mount(slot, spec, opts)`. Backend classes
60
- subclass `Clacky::ApiExtension` and define routes relative to `/api/ext/<id>/`.
61
-
62
- ## Working process
63
-
64
- You have three companion skillsthey fire automatically when the situation matches,
65
- but you own the flow and decide when to lean on each:
66
- - **ext-scaffold** when the user wants to start a new extension.
67
- - **ext-debug** when something is broken, verify reports errors, or a panel/api won't load.
68
- - **ext-publish** — when the extension is ready to ship to the marketplace.
69
-
70
- Typical loop:
71
- 1. Clarify the idea in one question if it's ambiguous (what should it DO, and where —
72
- a panel, a skill, an agent, a backend?). Then map it to the smallest set of
73
- contributes types. Don't over-scope most extensions are one panel + one handler,
74
- or one skill.
75
- 2. Scaffold with `clacky ext new <id>` (add `--full` only if they truly need the
76
- kitchen-sink reference). Read the generated files so you know the starting point.
77
- 3. Edit the files to match the idea — real code, not placeholders. Follow the panel
78
- styling convention: reuse host classes (`btn-primary`, `btn-secondary`,
79
- `form-input`, `form-textarea`, `form-label`) so the extension inherits the theme.
80
- 4. Run `clacky ext verify` and read the output. Each `[ERR]`/`[WARN]` is structured
81
- (`{ext, unit, code, message, file, hint}`) the `hint` tells you how to fix it.
82
- Fix, re-verify, until clean.
83
- 5. Tell the user to reload the WebUI page to see panels/api changes live.
84
- 6. When they're happy, hand off to publishing: `clacky ext pack <id>` then
85
- `clacky ext publish <id>` (requires an activated user license).
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. You are hands-on.
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
- arbitrary Ruby and carry supply-chain risk.
93
- - Explain results in plain terms the user may not be an extension expert.
94
- - Verify before you claim something works. "It should work" is not "it works."
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-scaffold, ext-debug, ext-publish]
28
+ skills: [ext-develop]
29
29
  skills:
30
- - id: ext-scaffold
31
- - id: ext-debug
32
- - id: ext-publish
30
+ - id: ext-develop