openclacky 0.9.19 → 0.9.21
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 +27 -0
- data/docs/c-end-user-positioning.md +64 -0
- data/lib/clacky/agent/llm_caller.rb +1 -1
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +4 -4
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +65 -58
- data/lib/clacky/default_skills/skill-creator/SKILL.md +6 -0
- data/lib/clacky/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/clacky/server/http_server.rb +60 -91
- data/lib/clacky/server/scheduler.rb +55 -0
- data/lib/clacky/tools/browser.rb +2 -2
- data/lib/clacky/utils/encoding.rb +21 -0
- data/lib/clacky/utils/environment_detector.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1 -1
- data/lib/clacky/web/tasks.js +10 -26
- data/scripts/install.ps1 +4 -4
- data/scripts/install.sh +50 -31
- metadata +3 -6
- data/lib/clacky/default_skills/cron-task-creator/scripts/list_tasks.rb +0 -121
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_schedule.rb +0 -149
- data/lib/clacky/default_skills/cron-task-creator/scripts/manage_task.rb +0 -81
- data/lib/clacky/default_skills/cron-task-creator/scripts/task_history.rb +0 -137
- data/scripts/install_simple.sh +0 -630
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d48d1b62badf0b608e9f6dc6cacb6b0d2c8f92263a8df77d64f6c63ee26e3580
|
|
4
|
+
data.tar.gz: f4abb3c3aa7dc140a91622135c4eaff7dfbad66bca6908abfb3c76ab7c183629
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acfdcca20845b40c076f1974dde40d5d62f194af03c14541a98c02c5fd431790df3e811ac17a0d35276938bb5475a53c3e22e8c8b9f56b87d0637f7a5168d7c5
|
|
7
|
+
data.tar.gz: a3ea45c455a7591917801963bb4a08587ea1ab24173d42cb8d1d2718dbf0a17fdf40e11296840dc107863d98e4aa1a2dad8008542b6214a10bda9d575fec7715
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.21] - 2026-03-30
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Feishu channel setup compatibility with v2.6**: fixed Ruby 3.1 syntax incompatibility in the Feishu setup script that caused failures on newer Feishu API versions
|
|
14
|
+
|
|
15
|
+
### Improved
|
|
16
|
+
- **skill-creator YAML validation**: added frontmatter schema validation for skill files, catching malformed skill definitions before they cause runtime errors
|
|
17
|
+
|
|
18
|
+
### More
|
|
19
|
+
- Removed `install_simple.sh` (consolidated into `install.sh`)
|
|
20
|
+
|
|
21
|
+
## [0.9.20] - 2026-03-30
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **SSL error retry**: LLM API calls now automatically retry on SSL errors (same as other network failures — up to 10 retries with 5s delay)
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **Brand wrapper not found under root**: the install script now places the brand command wrapper in the same directory as the `openclacky` binary, so it is always on PATH regardless of whether running as root or a normal user
|
|
28
|
+
|
|
29
|
+
### Improved
|
|
30
|
+
- **Cron task management refactored to API**: cron task CRUD operations now go through the HTTP API instead of running ad-hoc Ruby scripts, making the scheduler more reliable and easier to maintain
|
|
31
|
+
- **UTF-8 encoding fix for browser tool on Windows**: browser command output with non-ASCII characters no longer causes encoding errors
|
|
32
|
+
|
|
33
|
+
### More
|
|
34
|
+
- Installer no longer adds `~/.local/bin` to PATH (wrapper now colocated with gem binary, making the extra PATH entry unnecessary)
|
|
35
|
+
- Brand install tips in Windows PowerShell installer
|
|
36
|
+
|
|
10
37
|
## [0.9.19] - 2026-03-29
|
|
11
38
|
|
|
12
39
|
### Added
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# C-End User Positioning
|
|
2
|
+
|
|
3
|
+
> Date: 2026-03-30
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Market Context
|
|
8
|
+
|
|
9
|
+
The "OpenClaw ecosystem" has exploded in 2026. Key players:
|
|
10
|
+
|
|
11
|
+
- **OpenClaw** — open-source, self-hosted, community Skills. Designed for technical users who configure everything themselves.
|
|
12
|
+
- **QClaw** — Tencent's fork. Bundled Kimi model, WeChat binding. Mass-market but Tencent-ecosystem only.
|
|
13
|
+
- **Others** (Wukong, etc.) — same lane.
|
|
14
|
+
|
|
15
|
+
OpenClaw has 5,700+ Skills, but almost all are open-source, free, and easily copied. The ecosystem lacks **expertise-backed, production-grade Skills worth paying for**.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Who openclacky Is For
|
|
20
|
+
|
|
21
|
+
**Ordinary users, not technical geeks.**
|
|
22
|
+
|
|
23
|
+
The target user knows OpenClaw exists, has heard about "raising a lobster", but can't or doesn't want to:
|
|
24
|
+
- configure Docker / environment / webhooks
|
|
25
|
+
- manage their own API keys without knowing what they'll spend
|
|
26
|
+
- troubleshoot when a long task breaks halfway
|
|
27
|
+
|
|
28
|
+
They want to use a lobster built by an expert (a lawyer, a trader, an SEO specialist) — not build one themselves.
|
|
29
|
+
|
|
30
|
+
> Core insight: **OpenClaw is built for people who create Skills. openclacky is built for people who use them.**
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Why openclacky Over OpenClaw: 3 Core Reasons
|
|
35
|
+
|
|
36
|
+
### 1. Zero-friction IM setup — the strongest differentiator
|
|
37
|
+
|
|
38
|
+
OpenClaw requires users to manually configure webhooks, tokens, and config files to connect WeChat / Feishu / WeCom. High technical barrier, most ordinary users give up.
|
|
39
|
+
|
|
40
|
+
openclacky uses **AI-automated channel setup**: one sentence, and the AI configures the IM connection for you — no plugins, no docs, no engineering knowledge required. This is a genuine technical moat.
|
|
41
|
+
|
|
42
|
+
### 2. Built for China, natively
|
|
43
|
+
|
|
44
|
+
- No VPN required, no overseas credit card
|
|
45
|
+
- WeChat / Feishu / WeCom are the primary daily tools for Chinese users — openclacky treats them as first-class citizens
|
|
46
|
+
- Supports domestic models (DeepSeek, Kimi, etc.) out of the box
|
|
47
|
+
- QClaw is domestic too, but locked to Tencent's ecosystem and model choices
|
|
48
|
+
|
|
49
|
+
### 3. Cost transparency and long-task reliability
|
|
50
|
+
|
|
51
|
+
- Real-time token cost tracking — users always know what they're spending
|
|
52
|
+
- Automatic compression (up to 90% savings via Insert-then-Compress + Prompt Caching)
|
|
53
|
+
- Long tasks don't break: sub-agent isolation + Time Machine architecture keeps context intact
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## The User Progression
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Can use it → Dare to use it → Keep using it
|
|
61
|
+
(zero setup) (cost clarity) (tasks don't break)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Each of the 3 reasons maps directly to one stage of this progression.
|
|
@@ -34,7 +34,7 @@ module Clacky
|
|
|
34
34
|
max_tokens: @config.max_tokens,
|
|
35
35
|
enable_caching: @config.enable_prompt_caching
|
|
36
36
|
)
|
|
37
|
-
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
37
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
38
38
|
@ui&.clear_progress
|
|
39
39
|
retries += 1
|
|
40
40
|
if retries <= max_retries
|
|
@@ -71,9 +71,9 @@ BOT_PERMISSIONS = %w[
|
|
|
71
71
|
# Logging helpers
|
|
72
72
|
# ---------------------------------------------------------------------------
|
|
73
73
|
|
|
74
|
-
def step(msg)
|
|
75
|
-
def ok(msg)
|
|
76
|
-
def warn(msg)
|
|
74
|
+
def step(msg); puts("[feishu-setup] #{msg}"); end
|
|
75
|
+
def ok(msg); puts("[feishu-setup] ✅ #{msg}"); end
|
|
76
|
+
def warn(msg); puts("[feishu-setup] ⚠️ #{msg}"); end
|
|
77
77
|
def fail!(msg)
|
|
78
78
|
puts("[feishu-setup] ❌ #{msg}")
|
|
79
79
|
exit 1
|
|
@@ -501,7 +501,7 @@ def run_setup(browser, api)
|
|
|
501
501
|
id = s["id"].to_s
|
|
502
502
|
name_to_id[name] = id if name && !id.empty?
|
|
503
503
|
end
|
|
504
|
-
ids = BOT_PERMISSIONS.
|
|
504
|
+
ids = BOT_PERMISSIONS.map { |n| name_to_id[n] }.compact
|
|
505
505
|
missing = BOT_PERMISSIONS.reject { |n| name_to_id.key?(n) }
|
|
506
506
|
warn "#{missing.size} permissions not matched: #{missing.join(", ")}" unless missing.empty?
|
|
507
507
|
fail! "No permission IDs matched. API response keys: #{name_to_id.keys.first(5).inspect}" if ids.empty?
|
|
@@ -17,21 +17,14 @@ Storage:
|
|
|
17
17
|
~/.clacky/schedules.yml # All scheduled plans (YAML list)
|
|
18
18
|
~/.clacky/logger/clacky-*.log # Execution logs (daily rotation)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
POST /api/tasks → create task {name, content}
|
|
29
|
-
GET /api/tasks/:name → get task content
|
|
30
|
-
DELETE /api/tasks/:name → delete task
|
|
31
|
-
POST /api/tasks/run → run immediately {name}
|
|
32
|
-
GET /api/schedules → list all schedules
|
|
33
|
-
POST /api/schedules → create schedule {name, task, cron}
|
|
34
|
-
DELETE /api/schedules/:name → delete schedule
|
|
20
|
+
API Base: http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}
|
|
21
|
+
|
|
22
|
+
Cron-Tasks API (unified — manages task file + schedule together):
|
|
23
|
+
GET /api/cron-tasks → list all cron tasks with schedule info
|
|
24
|
+
POST /api/cron-tasks → create task + schedule {name, content, cron, enabled?}
|
|
25
|
+
PATCH /api/cron-tasks/:name → update {content?, cron?, enabled?}
|
|
26
|
+
DELETE /api/cron-tasks/:name → delete task file + schedule
|
|
27
|
+
POST /api/cron-tasks/:name/run → execute immediately (creates a new session)
|
|
35
28
|
```
|
|
36
29
|
|
|
37
30
|
## Cron Expression Quick Reference
|
|
@@ -54,10 +47,11 @@ Field order: `minute hour day-of-month month day-of-week`
|
|
|
54
47
|
|
|
55
48
|
### 1. LIST — Show all tasks
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
```bash
|
|
51
|
+
curl -s http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks
|
|
52
|
+
```
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
2. Display results: task name, cron schedule, enabled status, last run status
|
|
54
|
+
Display each task: name, cron schedule, enabled status, content preview.
|
|
61
55
|
|
|
62
56
|
If no tasks exist, inform the user and offer to create one or show templates.
|
|
63
57
|
|
|
@@ -69,15 +63,14 @@ If no tasks exist, inform the user and offer to create one or show templates.
|
|
|
69
63
|
|
|
70
64
|
**Step 1: Gather required info** (only ask for what's missing)
|
|
71
65
|
- What should the task DO? (goal, behavior, output format)
|
|
72
|
-
- How often should it run? (or is it manual-only?)
|
|
66
|
+
- How often should it run? (or is it manual-only without a schedule?)
|
|
73
67
|
- Any specific parameters? (URLs, file paths, output location, language)
|
|
74
68
|
|
|
75
69
|
**Step 2: Generate task name**
|
|
76
70
|
- Rule: only `[a-z0-9_-]`, lowercase, no spaces
|
|
77
71
|
- Examples: `daily_report`, `price_monitor`, `weekly_summary`
|
|
78
72
|
|
|
79
|
-
**Step 3: Write the task prompt
|
|
80
|
-
Path: `~/.clacky/tasks/<name>.md`
|
|
73
|
+
**Step 3: Write the task prompt**
|
|
81
74
|
|
|
82
75
|
The prompt must be:
|
|
83
76
|
- **Self-contained**: the agent running it has zero prior context — include everything needed
|
|
@@ -85,7 +78,7 @@ The prompt must be:
|
|
|
85
78
|
- **Detailed**: include URLs, file paths, output format, language, expected output location
|
|
86
79
|
|
|
87
80
|
Good task prompt example:
|
|
88
|
-
```
|
|
81
|
+
```
|
|
89
82
|
You are a price monitoring assistant. Complete the following task:
|
|
90
83
|
|
|
91
84
|
## Goal
|
|
@@ -100,14 +93,17 @@ Check the current BTC price on CoinGecko, compare with yesterday's price, and lo
|
|
|
100
93
|
Execute immediately.
|
|
101
94
|
```
|
|
102
95
|
|
|
103
|
-
**Step 4:
|
|
96
|
+
**Step 4: Create via API**
|
|
104
97
|
|
|
105
98
|
```bash
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks \
|
|
100
|
+
-H "Content-Type: application/json" \
|
|
101
|
+
-d '{
|
|
102
|
+
"name": "task_name",
|
|
103
|
+
"content": "task prompt content...",
|
|
104
|
+
"cron": "0 9 * * *",
|
|
105
|
+
"enabled": true
|
|
106
|
+
}'
|
|
111
107
|
```
|
|
112
108
|
|
|
113
109
|
**Step 5: Confirm creation**
|
|
@@ -116,7 +112,6 @@ ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb add "<name>"
|
|
|
116
112
|
✅ Task created successfully!
|
|
117
113
|
|
|
118
114
|
📋 Task name: daily_standup
|
|
119
|
-
📄 File: ~/.clacky/tasks/daily_standup.md
|
|
120
115
|
⏰ Schedule: Weekdays at 09:00 (cron: 0 9 * * 1-5)
|
|
121
116
|
|
|
122
117
|
View and manage this task in the Clacky WebUI → Tasks panel. Click ▶ Run to execute immediately.
|
|
@@ -126,24 +121,27 @@ View and manage this task in the Clacky WebUI → Tasks panel. Click ▶ Run to
|
|
|
126
121
|
|
|
127
122
|
### 3. EDIT — Modify an existing task
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
**Step 1**: Identify the task (if unclear, LIST first and ask)
|
|
130
125
|
|
|
131
|
-
**Step
|
|
126
|
+
**Step 2**: Show current state via LIST or ask user to confirm
|
|
132
127
|
|
|
133
|
-
**Step
|
|
134
|
-
|
|
135
|
-
**Step 3**: Determine what to change
|
|
136
|
-
- Prompt only → rewrite `~/.clacky/tasks/<name>.md`
|
|
137
|
-
- Schedule only → update `cron` field in `schedules.yml`
|
|
138
|
-
- Both → do both
|
|
139
|
-
- Rename → rename .md file + update schedules.yml
|
|
128
|
+
**Step 3**: Update via API
|
|
140
129
|
|
|
141
130
|
```bash
|
|
142
|
-
# Update
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
# Update content only
|
|
132
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
133
|
+
-H "Content-Type: application/json" \
|
|
134
|
+
-d '{"content": "new prompt content..."}'
|
|
135
|
+
|
|
136
|
+
# Update cron schedule only
|
|
137
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
138
|
+
-H "Content-Type: application/json" \
|
|
139
|
+
-d '{"cron": "0 8 * * 1-5"}'
|
|
140
|
+
|
|
141
|
+
# Update both
|
|
142
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
143
|
+
-H "Content-Type: application/json" \
|
|
144
|
+
-d '{"content": "...", "cron": "0 8 * * 1-5"}'
|
|
147
145
|
```
|
|
148
146
|
|
|
149
147
|
**Step 4**: Confirm changes
|
|
@@ -151,7 +149,6 @@ ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb update "<sche
|
|
|
151
149
|
```
|
|
152
150
|
✅ Task updated!
|
|
153
151
|
📋 daily_standup
|
|
154
|
-
Prompt: updated ✓
|
|
155
152
|
Schedule: 0 9 * * 1-5 → 0 8 * * 1-5 (now weekdays at 08:00)
|
|
156
153
|
```
|
|
157
154
|
|
|
@@ -160,7 +157,15 @@ ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb update "<sche
|
|
|
160
157
|
### 4. ENABLE / DISABLE — Toggle a task
|
|
161
158
|
|
|
162
159
|
```bash
|
|
163
|
-
|
|
160
|
+
# Disable
|
|
161
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
162
|
+
-H "Content-Type: application/json" \
|
|
163
|
+
-d '{"enabled": false}'
|
|
164
|
+
|
|
165
|
+
# Enable
|
|
166
|
+
curl -s -X PATCH http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name \
|
|
167
|
+
-H "Content-Type: application/json" \
|
|
168
|
+
-d '{"enabled": true}'
|
|
164
169
|
```
|
|
165
170
|
|
|
166
171
|
Confirm:
|
|
@@ -180,15 +185,22 @@ Always confirm before deleting (unless the user has explicitly said to delete):
|
|
|
180
185
|
```
|
|
181
186
|
|
|
182
187
|
```bash
|
|
183
|
-
|
|
188
|
+
curl -s -X DELETE http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name
|
|
184
189
|
```
|
|
185
190
|
|
|
186
191
|
---
|
|
187
192
|
|
|
188
193
|
### 6. HISTORY — View run history
|
|
189
194
|
|
|
195
|
+
Read the daily log files directly:
|
|
196
|
+
|
|
190
197
|
```bash
|
|
191
|
-
|
|
198
|
+
grep "task_name" ~/.clacky/logger/clacky-$(date +%Y-%m-%d).log | tail -20
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Or search across recent days:
|
|
202
|
+
```bash
|
|
203
|
+
grep -h "task_name" ~/.clacky/logger/clacky-*.log | tail -30
|
|
192
204
|
```
|
|
193
205
|
|
|
194
206
|
Display format:
|
|
@@ -198,24 +210,21 @@ Display format:
|
|
|
198
210
|
Mar 10 19:00 ❌ Failed — JSON::ParserError: unexpected end of input
|
|
199
211
|
Mar 09 19:00 ✅ Success — took 1m 42s
|
|
200
212
|
Mar 08 19:00 ✅ Success — took 2m 10s
|
|
201
|
-
Mar 07 19:00 ⚠️ Skipped — (task was disabled)
|
|
202
|
-
|
|
203
|
-
Recent error:
|
|
204
|
-
JSON::ParserError at line 226
|
|
205
|
-
Possible cause: API response was truncated or empty
|
|
206
213
|
```
|
|
207
214
|
|
|
208
215
|
---
|
|
209
216
|
|
|
210
217
|
### 7. RUN NOW — Execute immediately
|
|
211
218
|
|
|
219
|
+
```bash
|
|
220
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks/task_name/run
|
|
212
221
|
```
|
|
213
|
-
▶️ Go to the Clacky WebUI → Tasks panel, find the task, and click ▶ Run.
|
|
214
222
|
|
|
215
|
-
|
|
223
|
+
This creates a new session. Tell the user:
|
|
224
|
+
```
|
|
225
|
+
▶️ Task started in a new session.
|
|
226
|
+
View it in the Clacky WebUI → Sessions panel.
|
|
216
227
|
```
|
|
217
|
-
|
|
218
|
-
If the user wants to run the task in the current session, read the task file and execute the instructions directly.
|
|
219
228
|
|
|
220
229
|
---
|
|
221
230
|
|
|
@@ -243,8 +252,6 @@ Tell me which one interests you, or describe your own use case!
|
|
|
243
252
|
## Important Notes
|
|
244
253
|
|
|
245
254
|
- Task names: only `[a-z0-9_-]`, no spaces, no uppercase
|
|
246
|
-
- When modifying `schedules.yml`: always read → modify → write back the full file (never append directly)
|
|
247
255
|
- Task prompt files must be **self-contained** — the executing agent has no prior memory
|
|
248
256
|
- Clacky server must be running for cron to trigger automatically (checked every minute)
|
|
249
257
|
- The WebUI Task Panel is the preferred interface for managing tasks — always remind the user to check it after changes
|
|
250
|
-
- Path expansion: always use absolute paths, expand `~` to the actual home directory
|
|
@@ -86,6 +86,12 @@ Components to fill in:
|
|
|
86
86
|
>
|
|
87
87
|
> **YAML description gotcha**: If the description contains `word: value` patterns (colons followed by space), YAML treats them as key-value pairs and the frontmatter parse fails silently. Always wrap description values in single quotes. Avoid embedded double-quotes inside single-quoted strings (use rephrasing instead).
|
|
88
88
|
|
|
89
|
+
> **After writing SKILL.md — always validate and auto-fix**: Run this immediately after creating or updating any skill file:
|
|
90
|
+
> ```bash
|
|
91
|
+
> ruby SKILL_DIR/scripts/validate_skill_frontmatter.rb /path/to/new-skill/SKILL.md
|
|
92
|
+
> ```
|
|
93
|
+
> The script validates the YAML frontmatter and auto-fixes common issues (unquoted descriptions, multi-line block scalars with colons). If it prints `OK:` — you're done. If it prints `Auto-fixed and saved` — it repaired the file automatically. If it prints `ERROR` — manual fix required.
|
|
94
|
+
|
|
89
95
|
### Skill Writing Guide
|
|
90
96
|
|
|
91
97
|
#### Anatomy of a Skill
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# validate_skill_frontmatter.rb
|
|
5
|
+
#
|
|
6
|
+
# Validates and auto-fixes the YAML frontmatter of a SKILL.md file.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby validate_skill_frontmatter.rb <path/to/SKILL.md>
|
|
10
|
+
#
|
|
11
|
+
# What it does:
|
|
12
|
+
# 1. Parses the frontmatter between --- delimiters
|
|
13
|
+
# 2. If YAML is invalid OR description is not a plain String:
|
|
14
|
+
# - Extracts name/description via regex fallback
|
|
15
|
+
# - Re-wraps description in single quotes (collapsed to one line)
|
|
16
|
+
# - Rewrites the frontmatter in the file
|
|
17
|
+
# 3. Exits 0 on success (with or without auto-fix), 1 on unrecoverable error
|
|
18
|
+
|
|
19
|
+
require "yaml"
|
|
20
|
+
|
|
21
|
+
path = ARGV[0]
|
|
22
|
+
|
|
23
|
+
if path.nil? || path.strip.empty?
|
|
24
|
+
warn "Usage: ruby validate_skill_frontmatter.rb <path/to/SKILL.md>"
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless File.exist?(path)
|
|
29
|
+
warn "File not found: #{path}"
|
|
30
|
+
exit 1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
content = File.read(path)
|
|
34
|
+
|
|
35
|
+
# Extract frontmatter block
|
|
36
|
+
fm_match = content.match(/\A(---\n)(.*?)(\n---[ \t]*\n?)/m)
|
|
37
|
+
unless fm_match
|
|
38
|
+
warn "ERROR: No frontmatter block found in #{path}"
|
|
39
|
+
exit 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
prefix = fm_match[1] # "---\n"
|
|
43
|
+
yaml_raw = fm_match[2] # raw YAML text
|
|
44
|
+
suffix = fm_match[3] # "\n---\n"
|
|
45
|
+
body = content[fm_match.end(0)..] # rest of file after frontmatter
|
|
46
|
+
|
|
47
|
+
# Attempt normal YAML parse
|
|
48
|
+
parse_ok = false
|
|
49
|
+
data = nil
|
|
50
|
+
begin
|
|
51
|
+
data = YAML.safe_load(yaml_raw) || {}
|
|
52
|
+
parse_ok = data["description"].is_a?(String)
|
|
53
|
+
rescue Psych::Exception => e
|
|
54
|
+
warn "YAML parse error: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if parse_ok
|
|
58
|
+
puts "OK: name=#{data['name'].inspect} description_length=#{data['description'].length}"
|
|
59
|
+
exit 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# --- Auto-fix ---
|
|
63
|
+
puts "Frontmatter invalid or description broken — attempting auto-fix..."
|
|
64
|
+
|
|
65
|
+
# Regex fallback: extract name and description lines
|
|
66
|
+
name_match = yaml_raw.match(/^name:\s*(.+)$/)
|
|
67
|
+
unless name_match
|
|
68
|
+
warn "ERROR: Cannot extract 'name' field from frontmatter. Manual fix required."
|
|
69
|
+
exit 1
|
|
70
|
+
end
|
|
71
|
+
name_value = name_match[1].strip.gsub(/\A['"]|['"]\z/, "")
|
|
72
|
+
|
|
73
|
+
# description may be:
|
|
74
|
+
# description: some text (unquoted)
|
|
75
|
+
# description: 'some text' (single-quoted)
|
|
76
|
+
# description: "some text" (double-quoted)
|
|
77
|
+
# description: first line\n continuation (multi-line block scalar)
|
|
78
|
+
desc_match = yaml_raw.match(/^description:\s*(.+?)(?=\n[a-z]|\z)/m)
|
|
79
|
+
unless desc_match
|
|
80
|
+
warn "ERROR: Cannot extract 'description' field from frontmatter. Manual fix required."
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raw_desc = desc_match[1].strip
|
|
85
|
+
|
|
86
|
+
# Strip existing outer quotes if present (simple single-line quoted values)
|
|
87
|
+
if raw_desc.start_with?("'") && raw_desc.end_with?("'")
|
|
88
|
+
raw_desc = raw_desc[1..-2]
|
|
89
|
+
elsif raw_desc.start_with?('"') && raw_desc.end_with?('"')
|
|
90
|
+
raw_desc = raw_desc[1..-2]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Collapse multi-line: strip leading whitespace from continuation lines
|
|
94
|
+
description_value = raw_desc.gsub(/\n\s+/, " ").strip
|
|
95
|
+
|
|
96
|
+
# Escape any single quotes inside the description value
|
|
97
|
+
description_value_escaped = description_value.gsub("'", "''")
|
|
98
|
+
|
|
99
|
+
# Extract all other frontmatter lines (everything except name: and description:)
|
|
100
|
+
other_lines = yaml_raw.each_line.reject do |line|
|
|
101
|
+
line.match?(/^(name|description):/) || line.match?(/^\s+\S/) && yaml_raw.match?(/^description:.*\n(\s+.+\n)*/m)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# More precise: collect lines that are not part of the name/description block
|
|
105
|
+
remaining = []
|
|
106
|
+
skip_continuation = false
|
|
107
|
+
yaml_raw.each_line do |line|
|
|
108
|
+
if line.match?(/^(name|description):/)
|
|
109
|
+
skip_continuation = true
|
|
110
|
+
next
|
|
111
|
+
end
|
|
112
|
+
if skip_continuation && line.match?(/^\s+\S/)
|
|
113
|
+
next # continuation of a multi-line block value
|
|
114
|
+
end
|
|
115
|
+
skip_continuation = false
|
|
116
|
+
remaining << line unless line.strip.empty? && remaining.empty?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Rebuild frontmatter
|
|
120
|
+
fixed_fm_lines = []
|
|
121
|
+
fixed_fm_lines << "name: #{name_value}"
|
|
122
|
+
fixed_fm_lines << "description: '#{description_value_escaped}'"
|
|
123
|
+
remaining.each { |l| fixed_fm_lines << l.chomp }
|
|
124
|
+
|
|
125
|
+
# Remove trailing blank lines from remaining
|
|
126
|
+
fixed_fm = fixed_fm_lines.join("\n").strip
|
|
127
|
+
|
|
128
|
+
new_content = "#{prefix}#{fixed_fm}#{suffix}#{body}"
|
|
129
|
+
|
|
130
|
+
File.write(path, new_content)
|
|
131
|
+
puts "Auto-fixed and saved: #{path}"
|
|
132
|
+
|
|
133
|
+
# Final verification
|
|
134
|
+
begin
|
|
135
|
+
verify_content = File.read(path)
|
|
136
|
+
verify_match = verify_content.match(/\A---\n(.*?)\n---/m)
|
|
137
|
+
verify_data = YAML.safe_load(verify_match[1])
|
|
138
|
+
raise "description not a String" unless verify_data["description"].is_a?(String)
|
|
139
|
+
puts "OK: name=#{verify_data['name'].inspect} description_length=#{verify_data['description'].length}"
|
|
140
|
+
rescue => e
|
|
141
|
+
warn "ERROR: Auto-fix failed, manual intervention required: #{e.message}"
|
|
142
|
+
exit 1
|
|
143
|
+
end
|