openclacky 0.9.30 → 0.9.32

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/lib/clacky/agent/llm_caller.rb +5 -5
  4. data/lib/clacky/agent/memory_updater.rb +1 -1
  5. data/lib/clacky/agent/session_serializer.rb +2 -1
  6. data/lib/clacky/agent/skill_auto_creator.rb +119 -0
  7. data/lib/clacky/agent/skill_evolution.rb +46 -0
  8. data/lib/clacky/agent/skill_manager.rb +8 -0
  9. data/lib/clacky/agent/skill_reflector.rb +97 -0
  10. data/lib/clacky/agent.rb +38 -12
  11. data/lib/clacky/agent_config.rb +10 -1
  12. data/lib/clacky/brand_config.rb +23 -0
  13. data/lib/clacky/cli.rb +1 -1
  14. data/lib/clacky/default_skills/onboard/SKILL.md +15 -7
  15. data/lib/clacky/default_skills/personal-website/publish.rb +1 -1
  16. data/lib/clacky/default_skills/skill-creator/SKILL.md +46 -0
  17. data/lib/clacky/json_ui_controller.rb +0 -4
  18. data/lib/clacky/message_history.rb +0 -12
  19. data/lib/clacky/plain_ui_controller.rb +19 -1
  20. data/lib/clacky/platform_http_client.rb +2 -4
  21. data/lib/clacky/providers.rb +12 -1
  22. data/lib/clacky/server/channel/channel_ui_controller.rb +0 -2
  23. data/lib/clacky/server/http_server.rb +13 -1
  24. data/lib/clacky/server/web_ui_controller.rb +55 -29
  25. data/lib/clacky/tools/shell.rb +91 -170
  26. data/lib/clacky/ui2/ui_controller.rb +100 -93
  27. data/lib/clacky/ui_interface.rb +0 -1
  28. data/lib/clacky/utils/arguments_parser.rb +5 -2
  29. data/lib/clacky/utils/limit_stack.rb +81 -13
  30. data/lib/clacky/version.rb +1 -1
  31. data/lib/clacky/web/app.css +247 -51
  32. data/lib/clacky/web/app.js +11 -3
  33. data/lib/clacky/web/brand.js +21 -3
  34. data/lib/clacky/web/creator.js +13 -2
  35. data/lib/clacky/web/i18n.js +41 -15
  36. data/lib/clacky/web/index.html +38 -20
  37. data/lib/clacky/web/sessions.js +256 -57
  38. data/lib/clacky/web/settings.js +32 -0
  39. data/lib/clacky/web/skills.js +61 -1
  40. metadata +4 -1
@@ -7,6 +7,15 @@ description: Create new skills, modify and improve existing skills, and measure
7
7
 
8
8
  A skill for creating new skills and iteratively improving them.
9
9
 
10
+ ## Usage Modes
11
+
12
+ This skill supports two modes:
13
+
14
+ ### 1. Interactive Mode (default)
15
+
16
+ The full workflow with user interviews, test cases, and iteration cycles.
17
+ Use when creating or refining skills manually.
18
+
10
19
  At a high level, the process of creating a skill goes like this:
11
20
 
12
21
  - Decide what you want the skill to do and roughly how it should do it
@@ -22,6 +31,43 @@ Your job is to figure out where the user is in this process and jump in to help
22
31
 
23
32
  Always be flexible. If the user says "skip the evals, just vibe with me", do that instead.
24
33
 
34
+ ### 2. Quick Mode (for agent self-evolution)
35
+
36
+ **Trigger**: When invoked with `mode: "quick"` in the task arguments.
37
+
38
+ Fast, opinionated skill creation without user interaction. This mode is used by the agent's self-evolution system to automatically create or improve skills.
39
+
40
+ **Behavior**:
41
+ - Skip user interviews and detailed requirements gathering
42
+ - Extract workflow pattern from provided context
43
+ - Write a minimal but functional SKILL.md
44
+ - Save to `~/.clacky/skills/auto-<name>-<timestamp>/` (or improve existing skill in place)
45
+ - Skip test cases and evals (user can refine later if needed)
46
+ - Always validate frontmatter with the validator script after creation
47
+ - Focus on the happy path; edge cases can be added later
48
+
49
+ **Expected arguments when using quick mode**:
50
+ - `task`: Clear description of what to automate and how (be specific about workflow steps)
51
+ - `mode`: Must be set to `"quick"`
52
+ - `suggested_name`: (optional) Proposed skill identifier (lowercase, hyphens OK)
53
+
54
+ **Quick mode principles**:
55
+ - **Be opinionated**: Make reasonable assumptions without asking
56
+ - **Be concise**: Keep instructions simple and focused
57
+ - **Be practical**: Focus on the core workflow that will save the most time
58
+ - **Be correct**: Always set `disable-model-invocation: false` and `user-invocable: true`
59
+ - **Be validating**: Run the frontmatter validator immediately after creation
60
+
61
+ **Example invocation from the agent's self-evolution system**:
62
+ ```
63
+ invoke_skill(
64
+ skill_name: "skill-creator",
65
+ task: "Create a skill to extract and summarize content from URLs. The skill should: 1) fetch the URL using safe_shell with curl, 2) parse the HTML to extract main text content, 3) generate a concise markdown summary. Expected input: URL string. Expected output: markdown summary with title and key points.",
66
+ mode: "quick",
67
+ suggested_name: "url-summarizer"
68
+ )
69
+ ```
70
+
25
71
  ---
26
72
 
27
73
  ## Platform Context: Clacky
@@ -132,10 +132,6 @@ module Clacky
132
132
  @progress_start_time = nil if phase == "done"
133
133
  end
134
134
 
135
- def clear_progress
136
- show_progress(progress_type: "thinking", phase: "done")
137
- end
138
-
139
135
  # === State updates ===
140
136
 
141
137
  def update_sessionbar(tasks: nil, cost: nil, status: nil)
@@ -62,12 +62,6 @@ module Clacky
62
62
  @messages.pop
63
63
  end
64
64
 
65
- # Remove messages from the end while the block is truthy.
66
- def pop_while(&block)
67
- @messages.pop while !@messages.empty? && block.call(@messages.last)
68
- self
69
- end
70
-
71
65
  # Remove all messages matching the block in-place
72
66
  # (e.g. cleanup_memory_messages uses reject! { m[:memory_update] }).
73
67
  def delete_where(&block)
@@ -152,12 +146,6 @@ module Clacky
152
146
  @messages.select { |m| !m[:task_id] || m[:task_id] <= task_id }
153
147
  end
154
148
 
155
- # Count how many of the last N messages have :truncated set.
156
- # Used by think() to guard against infinite truncation retry loops.
157
- def recent_truncation_count(n)
158
- @messages.last(n).count { |m| m[:truncated] }
159
- end
160
-
161
149
  # ─────────────────────────────────────────────
162
150
  # Size helpers
163
151
  # ─────────────────────────────────────────────
@@ -23,6 +23,25 @@ module Clacky
23
23
 
24
24
  def show_tool_call(name, args)
25
25
  args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
26
+
27
+ # Special handling for request_user_feedback — display as a readable prompt
28
+ if name.to_s == "request_user_feedback"
29
+ question = args_data.is_a?(Hash) ? (args_data[:question] || args_data["question"]).to_s : ""
30
+ context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
31
+ options = args_data.is_a?(Hash) ? (args_data[:options] || args_data["options"]) : nil
32
+ options = Array(options) if options && !options.is_a?(Array)
33
+
34
+ parts = []
35
+ parts << "**Context:** #{context.strip}" if context && !context.strip.empty?
36
+ parts << "**Question:** #{question.strip}"
37
+ if options && !options.empty?
38
+ parts << "**Options:**"
39
+ options.each_with_index { |opt, i| parts << " #{i + 1}. #{opt}" }
40
+ end
41
+ puts_line(parts.join("\n"))
42
+ return
43
+ end
44
+
26
45
  display = case name
27
46
  when "shell", "safe_shell"
28
47
  cmd = args_data.is_a?(Hash) ? (args_data[:command] || args_data["command"]) : args_data
@@ -107,7 +126,6 @@ module Clacky
107
126
  # === Progress (no-ops — no spinner in plain mode) ===
108
127
 
109
128
  def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}); end
110
- def clear_progress; end
111
129
 
112
130
  # === State updates (no-ops) ===
113
131
 
@@ -9,8 +9,6 @@ module Clacky
9
9
  # OpenClacky platform API (www.openclacky.com and its fallback domain).
10
10
  #
11
11
  # Features:
12
- # - Primary domain: https://www.openclacky.com (EdgeOne CDN-accelerated)
13
- # - Fallback domain: https://openclacky-platform.clackyai.app (direct, no CDN)
14
12
  # - Automatic retry with exponential back-off on transient failures
15
13
  # - Transparent domain failover: if the primary domain times out or returns a
16
14
  # 5xx error, the request is automatically retried against the fallback domain
@@ -23,9 +21,9 @@ module Clacky
23
21
  # # or { success: false, error: "...", data: {} }
24
22
  class PlatformHttpClient
25
23
  # Primary CDN-accelerated endpoint
26
- PRIMARY_HOST = "https://www.openclacky.com"
24
+ PRIMARY_HOST = "https://www.openclacky.com"
27
25
  # Direct fallback — bypasses EdgeOne, used when the primary times out
28
- FALLBACK_HOST = "https://openclacky-platform.clackyai.app"
26
+ FALLBACK_HOST = "https://openclacky.up.railway.app"
29
27
 
30
28
  # Number of attempts per domain (1 = no retry within the same domain)
31
29
  ATTEMPTS_PER_HOST = 2
@@ -18,6 +18,7 @@ module Clacky
18
18
  "default_model" => "abs-claude-sonnet-4-5",
19
19
  "lite_model" => "abs-claude-haiku-4-5",
20
20
  "models" => [
21
+ "abs-claude-opus-4-7",
21
22
  "abs-claude-opus-4-6",
22
23
  "abs-claude-sonnet-4-6",
23
24
  "abs-claude-sonnet-4-5",
@@ -63,7 +64,7 @@ module Clacky
63
64
  "base_url" => "https://api.anthropic.com",
64
65
  "api" => "anthropic-messages",
65
66
  "default_model" => "claude-sonnet-4.6",
66
- "models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
67
+ "models" => ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
67
68
  "website_url" => "https://console.anthropic.com/settings/keys"
68
69
  }.freeze,
69
70
 
@@ -74,6 +75,7 @@ module Clacky
74
75
  "default_model" => "abs-claude-sonnet-4-5",
75
76
  "lite_model" => "abs-claude-haiku-4-5",
76
77
  "models" => [
78
+ "abs-claude-opus-4-7",
77
79
  "abs-claude-opus-4-6",
78
80
  "abs-claude-sonnet-4-6",
79
81
  "abs-claude-sonnet-4-5",
@@ -94,6 +96,15 @@ module Clacky
94
96
  "default_model" => "mimo-v2-pro",
95
97
  "models" => ["mimo-v2-pro", "mimo-v2-omni"],
96
98
  "website_url" => "https://platform.xiaomimimo.com/"
99
+ }.freeze,
100
+
101
+ "glm" => {
102
+ "name" => "GLM (ZhipuAI)",
103
+ "base_url" => "https://open.bigmodel.cn/api/paas/v4",
104
+ "api" => "openai-completions",
105
+ "default_model" => "glm-5.1",
106
+ "models" => ["glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
107
+ "website_url" => "https://open.bigmodel.cn/usercenter/apikeys"
97
108
  }.freeze
98
109
 
99
110
  }.freeze
@@ -142,8 +142,6 @@ module Clacky
142
142
  # Suppress — progress spinner has no IM equivalent
143
143
  end
144
144
 
145
- def clear_progress; end
146
-
147
145
  # === State updates (no-ops for IM) ===
148
146
 
149
147
  def update_sessionbar(tasks: nil, cost: nil, status: nil); end
@@ -281,7 +281,7 @@ module Clacky
281
281
 
282
282
  server.mount_proc("/") do |req, res|
283
283
  if req.path == "/" || req.path == "/index.html"
284
- product_name = Clacky::BrandConfig.load.product_name || "Clacky"
284
+ product_name = Clacky::BrandConfig.load.product_name || "OpenClacky"
285
285
  html = File.read(index_html_path).gsub("{{BRAND_NAME}}", product_name)
286
286
  res.status = 200
287
287
  res["Content-Type"] = "text/html; charset=utf-8"
@@ -375,6 +375,7 @@ module Clacky
375
375
  when ["GET", "/api/store/skills"] then api_store_skills(res)
376
376
  when ["GET", "/api/brand/status"] then api_brand_status(res)
377
377
  when ["POST", "/api/brand/activate"] then api_brand_activate(req, res)
378
+ when ["DELETE", "/api/brand/license"] then api_brand_deactivate(res)
378
379
  when ["GET", "/api/brand/skills"] then api_brand_skills(res)
379
380
  when ["GET", "/api/brand"] then api_brand_info(res)
380
381
  when ["GET", "/api/creator/skills"] then api_creator_skills(res)
@@ -680,6 +681,17 @@ module Clacky
680
681
  end
681
682
  end
682
683
 
684
+ # DELETE /api/brand/license
685
+ # Deactivates (unbinds) the current brand license and clears all brand state.
686
+ # Brand skills are removed from disk. Returns 200 on success.
687
+ private def api_brand_deactivate(res)
688
+ brand = Clacky::BrandConfig.load
689
+ result = brand.deactivate!
690
+ # Reload skill_loader without brand config so brand skills are no longer visible.
691
+ @skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: Clacky::BrandConfig.new({}))
692
+ json_response(res, 200, { ok: true })
693
+ end
694
+
683
695
  # GET /api/brand/skills
684
696
  # Fetches the brand skills list from the cloud, enriched with local installed version.
685
697
  # Returns 200 with skill list, or 403 when license is not activated.
@@ -92,11 +92,25 @@ module Clacky
92
92
  end
93
93
 
94
94
  def show_tool_call(name, args)
95
- # Skip request_user_feedback — its question is already shown as an assistant message
96
- return if name.to_s == "request_user_feedback"
97
-
98
95
  args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
99
96
 
97
+ # Special handling for request_user_feedback — emit a dedicated UI event
98
+ if name.to_s == "request_user_feedback"
99
+ question = args_data.is_a?(Hash) ? (args_data[:question] || args_data["question"]).to_s : ""
100
+ context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
101
+ options = args_data.is_a?(Hash) ? (args_data[:options] || args_data["options"]) : nil
102
+
103
+ # Normalize options to array (guard against malformed data)
104
+ options = Array(options) if options && !options.is_a?(Array)
105
+
106
+ emit("request_feedback",
107
+ question: question,
108
+ context: context,
109
+ options: options || [])
110
+ # Don't forward to IM subscribers — they get the formatted text version already
111
+ return
112
+ end
113
+
100
114
  # Generate a human-readable summary using the tool's format_call method
101
115
  summary = tool_call_summary(name, args_data)
102
116
 
@@ -179,13 +193,6 @@ module Clacky
179
193
  forward_to_subscribers { |sub| sub.show_info(message) }
180
194
  end
181
195
 
182
- # Emit a two-phase idle compression status update.
183
- # The frontend uses the same DOM element for both phases so it renders as one line.
184
- # phase: :start → show spinner message; phase: :end → update in-place with final result
185
- def show_idle_status(phase:, message:)
186
- emit("idle_status", phase: phase.to_s, message: message)
187
- end
188
-
189
196
  def show_warning(message)
190
197
  emit("warning", message: message)
191
198
  forward_to_subscribers { |sub| sub.show_warning(message) }
@@ -209,10 +216,23 @@ module Clacky
209
216
  # === Progress ===
210
217
 
211
218
  def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
212
- @progress_start_time = Time.now if phase == "active"
213
- @live_progress_message = message
214
- # Reset stdout buffer for each new command so re-subscribe only replays current run
215
- @live_stdout_buffer = [] if phase == "active"
219
+ if phase == "active"
220
+ @progress_start_time = Time.now
221
+ # Store complete progress state for replay when user switches back to this session
222
+ @live_progress_state = {
223
+ message: message,
224
+ progress_type: progress_type,
225
+ metadata: metadata
226
+ }
227
+ # Reset stdout buffer for each new command so re-subscribe only replays current run
228
+ @live_stdout_buffer = []
229
+ elsif phase == "done"
230
+ @live_tool_call = nil # command finished — nothing left to replay
231
+ # Keep @live_stdout_buffer intact — it will be reset on the next show_progress call.
232
+ # This allows a brief replay window even after the command finishes.
233
+ @live_progress_state = nil
234
+ @progress_start_time = nil
235
+ end
216
236
 
217
237
  data = {
218
238
  message: message,
@@ -221,12 +241,15 @@ module Clacky
221
241
  status: phase == "active" ? "start" : "stop" # backward compat
222
242
  }
223
243
  data[:metadata] = metadata unless metadata.empty?
244
+ # Always include started_at for "active" phase so the frontend can set the
245
+ # correct timer origin even on the very first event (not just replay).
246
+ if phase == "active" && @progress_start_time
247
+ data[:started_at] = (@progress_start_time.to_f * 1000).round
248
+ end
224
249
  data[:elapsed] = (Time.now - @progress_start_time).round(1) if phase == "done" && @progress_start_time
225
250
 
226
251
  emit("progress", **data)
227
252
  forward_to_subscribers { |sub| sub.show_progress(message) }
228
-
229
- @progress_start_time = nil if phase == "done"
230
253
  end
231
254
 
232
255
  # Stream shell stdout/stderr lines to the browser while a command is running.
@@ -241,17 +264,7 @@ module Clacky
241
264
  # Not forwarded to IM subscribers — too noisy
242
265
  end
243
266
 
244
- def clear_progress
245
- @live_tool_call = nil # command finished — nothing left to replay
246
- # Keep @live_stdout_buffer intact — it will be reset on the next show_progress call.
247
- # This allows a brief replay window even after the command finishes.
248
- show_progress(progress_type: "thinking", phase: "done")
249
- end
250
-
251
267
  # Replay in-progress command state to a newly (re-)subscribing browser tab.
252
- # Called by http_server.rb after the subscribe handshake when the session
253
- # is still running a shell command. Without this, switching sessions and
254
- # switching back results in a blank progress area — the progress event and
255
268
  # all tool_stdout lines that fired while the user was away are lost.
256
269
  # Replay live state when a client re-subscribes (e.g. after switching sessions).
257
270
  #
@@ -264,9 +277,22 @@ module Clacky
264
277
  # The frontend's appendToolStdout will attach to the last visible .tool-item
265
278
  # even when _liveLastToolItem is null (after the tab re-loaded).
266
279
  def replay_live_state
267
- return unless @live_progress_message
268
-
269
- emit("progress", message: @live_progress_message, status: "start")
280
+ return unless @live_progress_state
281
+
282
+ # Replay complete progress state (not just message).
283
+ # Include started_at (ms since epoch) so the frontend can resume the
284
+ # elapsed-time counter from the correct origin instead of resetting to 0.
285
+ state = @live_progress_state
286
+ started_at_ms = @progress_start_time ? (@progress_start_time.to_f * 1000).round : nil
287
+
288
+ emit("progress",
289
+ message: state[:message],
290
+ progress_type: state[:progress_type],
291
+ phase: "active",
292
+ status: "start",
293
+ metadata: state[:metadata] || {},
294
+ started_at: started_at_ms
295
+ )
270
296
 
271
297
  buf = @live_stdout_buffer
272
298
  emit("tool_stdout", lines: buf) if buf && !buf.empty?