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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b959a76ace0015ec6279b1572afa3e961d862d9c856c9fdecc98f72d9e190e59
4
- data.tar.gz: 53cc27fce3b47e83cb428955eb6591d53acc148cdab55721c7c32330b351fed0
3
+ metadata.gz: d48d1b62badf0b608e9f6dc6cacb6b0d2c8f92263a8df77d64f6c63ee26e3580
4
+ data.tar.gz: f4abb3c3aa7dc140a91622135c4eaff7dfbad66bca6908abfb3c76ab7c183629
5
5
  SHA512:
6
- metadata.gz: 5b0d27875497a290060cabfc409c5b8a18b90b95c8bec82157f8d1f8e63a25fc78e3d52bff8bcbf3bd9969f8e65090137dc7ce7b394ac35779412a51534a166e
7
- data.tar.gz: 2b08d76c821e84c7196b4bc6364323879669259895b30cb803e01e70c49a59f44a40f5f607f8a4d3e10a96bfaa47167c03ed991fcebe6a5dcb44d099b18bdccd
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) = puts("[feishu-setup] #{msg}")
75
- def ok(msg) = puts("[feishu-setup] ✅ #{msg}")
76
- def warn(msg) = puts("[feishu-setup] ⚠️ #{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.filter_map { |n| name_to_id[n] }
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
- WebUI Task Panel (Clacky Web Interface):
21
- - Automatically reads tasks/ + schedules.yml and renders the task list
22
- - Each row shows task name, cron expression, and content preview
23
- - Action buttons: Run (execute now), ✎ Edit (edit in session), ✕ (delete)
24
- - Run button triggers POST /api/tasks/run executes task in a new session
25
-
26
- API Endpoints (available when clacky server is running):
27
- GET /api/tasks list all tasks
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
- When user asks "what tasks do I have", "list scheduled tasks", etc.:
50
+ ```bash
51
+ curl -s http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/cron-tasks
52
+ ```
58
53
 
59
- 1. Run `ruby ~/.clacky/skills/cron-task-creator/scripts/list_tasks.rb`
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 file**
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
- ```markdown
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: Write file and add schedule**
96
+ **Step 4: Create via API**
104
97
 
105
98
  ```bash
106
- # Write task file
107
- ruby ~/.clacky/skills/cron-task-creator/scripts/manage_task.rb create "<name>" "<content>"
108
-
109
- # Add schedule (if applicable)
110
- ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb add "<name>" "<task_name>" "<cron_expr>"
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
- When the user wants to change task content, cron schedule, or rename:
124
+ **Step 1**: Identify the task (if unclear, LIST first and ask)
130
125
 
131
- **Step 1**: Identify the task to modify (if unclear, LIST first and ask)
126
+ **Step 2**: Show current state via LIST or ask user to confirm
132
127
 
133
- **Step 2**: Show current state (first 10 lines + cron expression)
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 task content
143
- ruby ~/.clacky/skills/cron-task-creator/scripts/manage_task.rb update "<name>" "<new_content>"
144
-
145
- # Update cron schedule
146
- ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb update "<schedule_name>" "<new_cron>"
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
- ruby ~/.clacky/skills/cron-task-creator/scripts/manage_schedule.rb toggle "<schedule_name>" true|false
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
- ruby ~/.clacky/skills/cron-task-creator/scripts/manage_task.rb delete "<name>"
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
- ruby ~/.clacky/skills/cron-task-creator/scripts/task_history.rb "<task_name>"
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
- The task will execute in a new session. You can watch it run in real time via the WebUI.
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