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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/lib/clacky/agent/llm_caller.rb +5 -5
- data/lib/clacky/agent/memory_updater.rb +1 -1
- data/lib/clacky/agent/session_serializer.rb +2 -1
- data/lib/clacky/agent/skill_auto_creator.rb +119 -0
- data/lib/clacky/agent/skill_evolution.rb +46 -0
- data/lib/clacky/agent/skill_manager.rb +8 -0
- data/lib/clacky/agent/skill_reflector.rb +97 -0
- data/lib/clacky/agent.rb +38 -12
- data/lib/clacky/agent_config.rb +10 -1
- data/lib/clacky/brand_config.rb +23 -0
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +15 -7
- data/lib/clacky/default_skills/personal-website/publish.rb +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +46 -0
- data/lib/clacky/json_ui_controller.rb +0 -4
- data/lib/clacky/message_history.rb +0 -12
- data/lib/clacky/plain_ui_controller.rb +19 -1
- data/lib/clacky/platform_http_client.rb +2 -4
- data/lib/clacky/providers.rb +12 -1
- data/lib/clacky/server/channel/channel_ui_controller.rb +0 -2
- data/lib/clacky/server/http_server.rb +13 -1
- data/lib/clacky/server/web_ui_controller.rb +55 -29
- data/lib/clacky/tools/shell.rb +91 -170
- data/lib/clacky/ui2/ui_controller.rb +100 -93
- data/lib/clacky/ui_interface.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +5 -2
- data/lib/clacky/utils/limit_stack.rb +81 -13
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +247 -51
- data/lib/clacky/web/app.js +11 -3
- data/lib/clacky/web/brand.js +21 -3
- data/lib/clacky/web/creator.js +13 -2
- data/lib/clacky/web/i18n.js +41 -15
- data/lib/clacky/web/index.html +38 -20
- data/lib/clacky/web/sessions.js +256 -57
- data/lib/clacky/web/settings.js +32 -0
- data/lib/clacky/web/skills.js +61 -1
- 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
|
|
24
|
+
PRIMARY_HOST = "https://www.openclacky.com"
|
|
27
25
|
# Direct fallback — bypasses EdgeOne, used when the primary times out
|
|
28
|
-
FALLBACK_HOST
|
|
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
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -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
|
|
@@ -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 || "
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 @
|
|
268
|
-
|
|
269
|
-
|
|
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?
|