Compare commits

...

10 Commits

Author SHA1 Message Date
daymade
edaeaa89f4 fix(pdf-creator): resolve CJK text garbled in weasyprint code blocks
weasyprint renders <pre> blocks with monospace fonts that lack CJK glyphs,
causing Chinese/Japanese/Korean characters to display as garbled text.

Fix: add _fix_cjk_code_blocks() preprocessor that detects CJK in <pre><code>
and converts to <div class="cjk-code-block"> with inherited body font.
Pure-ASCII code blocks are left untouched.

Also adds code/pre/pre-code CSS rules to both themes (default + warm-terra)
that were previously missing entirely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:01:13 +08:00
daymade
9242af5fcb Update transcript-fixer guidance and hook paths 2026-04-06 17:50:17 +08:00
daymade
000596dad6 chore(twitter-reader): bump version to 1.1.0
- Update description to reflect new fetch_article.py capabilities
- Add keywords: images, attachments, markdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:33:40 +08:00
daymade
22ec9f0d59 feat(twitter-reader): add fetch_article.py for X Articles with images
- Use twitter-cli for structured metadata (likes, retweets, bookmarks)
- Use Jina API for content with images
- Auto-download all images to attachments/
- Generate Markdown with YAML frontmatter and local image references
- Security scan passed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 16:31:33 +08:00
daymade
673980639b feat: add SKILL.md edit hook — warns to bump version in marketplace.json
Two hooks now active for marketplace-dev users:
1. Edit marketplace.json → auto-validate schema
2. Edit any SKILL.md → warn if version bump needed or skill unregistered

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 09:00:58 +08:00
daymade
c120cd415e feat: add PostToolUse hook to auto-validate marketplace.json on edit
When marketplace-dev is installed, any Write/Edit to a marketplace.json
automatically runs `claude plugin validate` and reports pass/fail.
Users get instant feedback without remembering to validate manually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:59:45 +08:00
daymade
1ff1499633 feat: add pre-flight checklist hooks to marketplace-dev skill
Sync check (skills ↔ marketplace.json), metadata audit,
per-plugin validation, and final claude plugin validate gate.
All users installing this skill get these process guards.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:58:12 +08:00
daymade
2097ffb527 feat: add marketplace-dev skill for converting skills repos to plugin marketplaces
Encodes proven methodology from real marketplace development:
- 4-phase workflow: Analyze → Create → Validate → PR
- 8 schema hard-rules (verified against Claude Code source)
- 13 anti-patterns from production debugging
- Complete marketplace.json schema reference
- Marketplace maintenance rules (version bumping, description updates)

Also fixes: remove invalid metadata.homepage from our own marketplace.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:56:48 +08:00
daymade
681994316b chore: bump transcript-fixer skill version 2026-04-06 08:50:10 +08:00
daymade
efda299a9e feat(cli-demo-generator): deep rewrite with battle-tested VHS patterns
SKILL.md: rewritten following Anthropic best practices
- Concise (233 lines, down from 347)
- Critical VHS parser limitations section (base64 workaround)
- Advanced patterns: self-bootstrap, output filtering, frame verification
- Better description for skill triggering

New files:
- references/advanced_patterns.md: production patterns from dbskill project
- assets/templates/self-bootstrap.tape: self-cleaning demo template

auto_generate_demo.py: new flags
- --bootstrap: hidden setup commands (self-cleaning state)
- --filter: regex pattern to filter noisy output
- --speed: post-processing speed multiplier (gifsicle)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 08:15:16 +08:00
24 changed files with 1780 additions and 401 deletions

View File

@@ -5,9 +5,8 @@
"email": "daymadev89@gmail.com" "email": "daymadev89@gmail.com"
}, },
"metadata": { "metadata": {
"description": "Professional Claude Code skills for GitHub operations, document conversion, diagram generation, statusline customization, Teams communication, repomix utilities, skill creation, CLI demo generation, LLM icon access, Cloudflare troubleshooting, UI design system extraction, professional presentation creation, YouTube video downloading, secure repomix packaging, ASR transcription correction, video comparison quality analysis, comprehensive QA testing infrastructure, prompt optimization with EARS methodology, session history recovery, local Claude session continuation from `.claude` artifacts, documentation cleanup, format-controlled deep research report generation with evidence tracking, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, iOS app development with XcodeGen and SwiftUI, fact-checking with automated corrections, Twitter/X content fetching, intelligent macOS disk space recovery, skill quality review and improvement, GitHub contribution strategy, complete internationalization/localization setup, plugin/skill troubleshooting with diagnostic tools, evidence-based competitor analysis with source citations, Windows Remote Desktop (AVD/W365) connection quality diagnosis with transport protocol analysis and log parsing, Tailscale+proxy conflict diagnosis with SSH tunnel SOP for remote development, multi-path parallel product analysis with cross-model test-time compute scaling, real financial data collection for US equities with validation and yfinance pitfall handling, advanced Excel automation for formatted workbook generation and complex xlsm parsing, macOS programmatic window screenshot capture workflows, and verified Scrapling CLI installation and web extraction workflows", "description": "Professional Claude Code skills for GitHub operations, document conversion, diagram generation, statusline customization, Teams communication, repomix utilities, skill creation, CLI demo generation, LLM icon access, Cloudflare troubleshooting, UI design system extraction, professional presentation creation, YouTube video downloading, secure repomix packaging, ASR transcription correction, video comparison quality analysis, comprehensive QA testing infrastructure, prompt optimization with EARS methodology, session history recovery, local Claude session continuation from `.claude` artifacts, documentation cleanup, format-controlled deep research report generation with evidence tracking, PDF generation with Chinese font support, CLAUDE.md progressive disclosure optimization, CCPM skill registry search and management, Promptfoo LLM evaluation framework, iOS app development with XcodeGen and SwiftUI, fact-checking with automated corrections, Twitter/X content fetching, intelligent macOS disk space recovery, skill quality review and improvement, GitHub contribution strategy, complete internationalization/localization setup, plugin/skill troubleshooting with diagnostic tools, evidence-based competitor analysis with source citations, Windows Remote Desktop (AVD/W365) connection quality diagnosis with transport protocol analysis and log parsing, Tailscale+proxy conflict diagnosis with SSH tunnel SOP for remote development, multi-path parallel product analysis with cross-model test-time compute scaling, real financial data collection for US equities with validation and yfinance pitfall handling, advanced Excel automation for formatted workbook generation and complex xlsm parsing, macOS programmatic window screenshot capture workflows, and verified Scrapling CLI installation and web extraction workflows, and plugin marketplace development for converting skills repos into official Claude Code marketplaces",
"version": "1.40.0", "version": "1.40.1"
"homepage": "https://github.com/daymade/claude-code-skills"
}, },
"plugins": [ "plugins": [
{ {
@@ -292,7 +291,7 @@
"description": "Corrects speech-to-text (ASR/STT) transcription errors using dictionary rules and native Claude AI corrections (no API key needed by default). Supports intelligent paragraph breaks, filler word reduction, interactive review, Chinese domain names, and iterative dictionary building. Use when users mention transcript correction, ASR errors, speech-to-text mistakes, homophone errors, or working with transcription files", "description": "Corrects speech-to-text (ASR/STT) transcription errors using dictionary rules and native Claude AI corrections (no API key needed by default). Supports intelligent paragraph breaks, filler word reduction, interactive review, Chinese domain names, and iterative dictionary building. Use when users mention transcript correction, ASR errors, speech-to-text mistakes, homophone errors, or working with transcription files",
"source": "./", "source": "./",
"strict": false, "strict": false,
"version": "1.3.0", "version": "1.4.0",
"category": "productivity", "category": "productivity",
"keywords": [ "keywords": [
"transcription", "transcription",
@@ -417,7 +416,7 @@
"description": "Create PDF documents from markdown with Chinese font support. Supports theme system (default for formal docs, warm-terra for training materials) and dual backend (weasyprint or Chrome). Triggers include convert to PDF, generate PDF, markdown to PDF, or printable documents", "description": "Create PDF documents from markdown with Chinese font support. Supports theme system (default for formal docs, warm-terra for training materials) and dual backend (weasyprint or Chrome). Triggers include convert to PDF, generate PDF, markdown to PDF, or printable documents",
"source": "./", "source": "./",
"strict": false, "strict": false,
"version": "1.2.0", "version": "1.3.0",
"category": "document-conversion", "category": "document-conversion",
"keywords": [ "keywords": [
"pdf", "pdf",
@@ -426,6 +425,7 @@
"chrome", "chrome",
"themes", "themes",
"chinese-fonts", "chinese-fonts",
"cjk",
"document-generation", "document-generation",
"legal", "legal",
"reports", "reports",
@@ -543,10 +543,10 @@
}, },
{ {
"name": "twitter-reader", "name": "twitter-reader",
"description": "Fetch Twitter/X post content by URL using jina.ai API to bypass JavaScript restrictions. Use when Claude needs to retrieve tweet content including author, timestamp, post text, images, and thread replies. Supports individual posts or batch fetching from x.com or twitter.com URLs", "description": "Fetch Twitter/X post content including long-form Articles with full images and metadata. Use when Claude needs to retrieve tweet/article content, author info, engagement metrics (likes, retweets, bookmarks), and embedded media. Supports individual posts and X Articles (long-form content). Automatically downloads all images to local attachments folder and generates complete Markdown with proper image references. Preferred over Jina for X Articles with images.",
"source": "./", "source": "./",
"strict": false, "strict": false,
"version": "1.0.0", "version": "1.1.0",
"category": "utilities", "category": "utilities",
"keywords": [ "keywords": [
"twitter", "twitter",
@@ -556,7 +556,10 @@
"content-fetching", "content-fetching",
"api", "api",
"scraping", "scraping",
"threads" "threads",
"images",
"attachments",
"markdown"
], ],
"skills": [ "skills": [
"./twitter-reader" "./twitter-reader"
@@ -945,6 +948,45 @@
"skills": [ "skills": [
"./asr-transcribe-to-text" "./asr-transcribe-to-text"
] ]
},
{
"name": "marketplace-dev",
"description": "Convert any Claude Code skills repository into an official plugin marketplace. Creates .claude-plugin/marketplace.json conforming to the Anthropic spec, validates it, tests installation, and creates a PR. Includes anti-patterns from real development experience.",
"source": "./",
"strict": false,
"version": "1.0.0",
"category": "developer-tools",
"keywords": [
"marketplace",
"plugin",
"distribution",
"packaging"
],
"skills": [
"./marketplace-dev"
],
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/marketplace-dev/scripts/post_edit_validate.sh"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/marketplace-dev/scripts/post_edit_sync_check.sh"
}
]
}
]
}
} }
] ]
} }

View File

@@ -1,346 +1,233 @@
--- ---
name: cli-demo-generator name: cli-demo-generator
description: This skill should be used when users want to create animated CLI demos, terminal recordings, or command-line demonstration GIFs. It supports both manual tape file creation and automated demo generation from command descriptions. Use when users mention creating demos, recording terminal sessions, or generating animated GIFs of CLI workflows. description: Generates professional animated CLI demos as GIFs using VHS terminal recordings. Handles tape file creation, self-bootstrapping demos with hidden setup, output noise filtering, post-processing speed-up, and frame-level verification. Use when users want to create terminal demos, record CLI workflows as GIFs, generate animated documentation, build demo tapes for README files, or need to showcase any command-line tool visually. Also triggers on "record terminal", "VHS tape", "demo GIF", "animate my CLI", or any request to visually demonstrate shell commands.
--- ---
# CLI Demo Generator # CLI Demo Generator
Generate professional animated CLI demos with ease. This skill supports both automated generation from command descriptions and manual control for custom demos. Create professional animated CLI demos. Four approaches, from fully automated to pixel-precise manual control.
## When to Use This Skill ## Quick Start
Trigger this skill when users request: **Simplest path** — give commands, get GIF:
- "Create a demo showing how to install my package"
- "Generate a CLI demo of these commands"
- "Make an animated GIF of my terminal workflow"
- "Record a terminal session and convert to GIF"
- "Batch generate demos from this config"
- "Create an interactive typing demo"
## Core Capabilities
### 1. Automated Demo Generation (Recommended)
Use the `auto_generate_demo.py` script for quick, automated demo creation. This is the easiest and most common approach.
**Basic Usage:**
```bash ```bash
scripts/auto_generate_demo.py \ python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "npm install my-package" \ -c "npm install my-package" \
-c "npm run build" \ -c "npm run build" \
-o demo.gif -o demo.gif
``` ```
**With Options:** **Self-bootstrapping demo** — for repeatable recordings that clean their own state:
```bash ```bash
scripts/auto_generate_demo.py \ python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "command1" \ -c "npm install my-package" \
-c "command2" \ -c "npm run build" \
-o demo.gif \
--bootstrap "npm uninstall my-package 2>/dev/null" \
--speed 2
```
## Critical: VHS Parser Limitations
VHS `Type` strings cannot contain `$`, `\"`, or backticks. These cause parse errors:
```tape
# FAILS — VHS parser rejects special chars
Type "echo \"hello $USER\""
Type "claude() { command claude \"$@\"; }"
```
**Workaround: base64 encode the command**, decode at runtime:
```bash
# 1. Encode your complex command
echo 'claude() { command claude "$@" 2>&1 | grep -v "noise"; }' | base64
# Output: Y2xhdWRlKCkgey4uLn0K
# 2. Use in tape
Type "echo Y2xhdWRlKCkgey4uLn0K | base64 -d > /tmp/wrapper.sh && source /tmp/wrapper.sh"
```
This pattern is essential for output filtering, function definitions, and any command with shell special characters.
## Approaches
### 1. Automated Generation (Recommended)
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/auto_generate_demo.py \
-c "command1" -c "command2" \
-o output.gif \ -o output.gif \
--title "Installation Demo" \ --title "My Demo" \
--theme "Dracula" \ --theme "Catppuccin Latte" \
--width 1400 \ --font-size 24 \
--height 700 --width 1400 --height 600
``` ```
**Script Parameters:** | Flag | Default | Description |
- `-c, --command`: Command to include (can be specified multiple times) |------|---------|-------------|
- `-o, --output`: Output GIF file path (required) | `-c` | required | Command to include (repeatable) |
- `--title`: Demo title (optional, shown at start) | `-o` | required | Output GIF path |
- `--theme`: VHS theme (default: Dracula) | `--title` | none | Title shown at start |
- `--font-size`: Font size (default: 16) | `--theme` | Dracula | VHS theme name |
- `--width`: Terminal width (default: 1400) | `--font-size` | 16 | Font size in pt |
- `--height`: Terminal height (default: 700) | `--width` | 1400 | Terminal width px |
- `--no-execute`: Generate tape file only, don't execute VHS | `--height` | 700 | Terminal height px |
| `--bootstrap` | none | Hidden setup command (repeatable) |
| `--filter` | none | Regex pattern to filter from output |
| `--speed` | 1 | Playback speed multiplier (uses gifsicle) |
| `--no-execute` | false | Generate .tape only |
**Smart Features:** Smart timing: `install`/`build`/`test`/`deploy` → 3s, `ls`/`pwd`/`echo` → 1s, others → 2s.
- Automatic timing based on command complexity
- Optimized sleep durations (1-3s depending on operation)
- Proper spacing between commands
- Professional defaults
### 2. Batch Demo Generation ### 2. Batch Generation
Use `batch_generate.py` for creating multiple demos from a configuration file. Create multiple demos from one config:
**Configuration File (YAML):**
```yaml ```yaml
# demos.yaml
demos: demos:
- name: "Install Demo" - name: "Install"
output: "install.gif" output: "install.gif"
title: "Installation" commands: ["npm install my-package"]
theme: "Dracula" - name: "Usage"
commands:
- "npm install my-package"
- "npm run build"
- name: "Usage Demo"
output: "usage.gif" output: "usage.gif"
commands: commands: ["my-package --help", "my-package run"]
- "my-package --help"
- "my-package run"
``` ```
**Usage:**
```bash ```bash
scripts/batch_generate.py config.yaml --output-dir ./demos python3 ${CLAUDE_SKILL_DIR}/scripts/batch_generate.py demos.yaml --output-dir ./gifs
``` ```
**When to Use Batch Generation:**
- Creating a suite of related demos
- Documenting multiple features
- Generating demos for tutorials or documentation
- Maintaining consistent demo series
### 3. Interactive Recording ### 3. Interactive Recording
Use `record_interactive.sh` for recording live terminal sessions. Record a live terminal session:
**Usage:**
```bash
scripts/record_interactive.sh output.gif \
--theme "Dracula" \
--width 1400
```
**Recording Process:**
1. Script starts asciinema recording
2. Type commands naturally in your terminal
3. Press Ctrl+D when finished
4. Script auto-converts to GIF via VHS
**When to Use Interactive Recording:**
- Demonstrating complex workflows
- Showing real command output
- Capturing live interactions
- Recording debugging sessions
### 4. Manual Tape File Creation
For maximum control, create VHS tape files manually using templates.
**Available Templates:**
- `assets/templates/basic.tape` - Simple command demo
- `assets/templates/interactive.tape` - Typing simulation
**Example Workflow:**
1. Copy template: `cp assets/templates/basic.tape my-demo.tape`
2. Edit commands and timing
3. Generate GIF: `vhs < my-demo.tape`
Consult `references/vhs_syntax.md` for complete VHS syntax reference.
## Workflow Guidance
### For Simple Demos (1-3 commands)
Use automated generation for quick results:
```bash ```bash
scripts/auto_generate_demo.py \ bash ${CLAUDE_SKILL_DIR}/scripts/record_interactive.sh output.gif --theme "Catppuccin Latte"
-c "echo 'Hello World'" \ # Type commands naturally, Ctrl+D when done
-c "ls -la" \
-o hello-demo.gif \
--title "Hello Demo"
``` ```
### For Multiple Related Demos Requires asciinema (`brew install asciinema`).
Create a batch configuration file and use batch generation: ### 4. Manual Tape File
1. Create `demos-config.yaml` with all demo definitions For maximum control, write a tape directly. Templates in `assets/templates/`:
2. Run: `scripts/batch_generate.py demos-config.yaml --output-dir ./output`
3. All demos generate automatically with consistent settings
### For Interactive/Complex Workflows - `basic.tape` — simple command sequence
- `interactive.tape` — typing simulation
- `self-bootstrap.tape`**self-cleaning demo with hidden setup** (recommended for repeatable demos)
Use interactive recording to capture real behavior: ## Advanced Patterns
These patterns come from production use. See `references/advanced_patterns.md` for full details.
### Self-Bootstrapping Demos
Demos that clean previous state, set up environment, and hide all of it from the viewer:
```tape
Hide
Type "cleanup-previous-state 2>/dev/null"
Enter
Sleep 2s
Type "clear"
Enter
Sleep 500ms
Show
Type "the-command-users-see"
Enter
Sleep 3s
```
The `Hide` → commands → `clear``Show` sequence is critical. `clear` wipes the terminal buffer so hidden commands don't leak into the GIF.
### Output Noise Filtering
Filter noisy progress lines from commands that produce verbose output:
```tape
# Hidden: create a wrapper function that filters noise
Hide
Type "echo <base64-encoded-wrapper> | base64 -d > /tmp/w.sh && source /tmp/w.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show
# Visible: clean command, filtered output
Type "my-noisy-command"
Enter
Sleep 3s
```
### Frame Verification
After recording, verify GIF content by extracting key frames:
```bash ```bash
scripts/record_interactive.sh my-workflow.gif # Extract frames at specific positions
# Type commands naturally ffmpeg -i demo.gif -vf "select=eq(n\,100)" -frames:v 1 /tmp/frame.png -y 2>/dev/null
# Ctrl+D when done
# View the frame (Claude can read images)
# Use Read tool on /tmp/frame.png to verify content
``` ```
### For Custom Timing/Layout ### Post-Processing Speed-Up
Create manual tape file with precise control: Use gifsicle to speed up recordings without re-recording:
1. Start with template or generate base tape with `--no-execute` ```bash
2. Edit timing, add comments, customize layout # 2x speed (halve frame delay)
3. Generate: `vhs < custom-demo.tape` gifsicle -d2 original.gif "#0-" > fast.gif
## Best Practices # 1.5x speed
gifsicle -d4 original.gif "#0-" > faster.gif
```
Refer to `references/best_practices.md` for comprehensive guidelines. Key recommendations: ### Template Placeholder Pattern
**Timing:** Keep tape files generic with placeholders, replace at build time:
- Quick commands (ls, pwd): 1s sleep
- Standard commands (grep, cat): 2s sleep
- Heavy operations (install, build): 3s+ sleep
**Sizing:** ```tape
- Standard: 1400x700 (recommended) # In tape file
- Compact: 1200x600 Type "claude plugin marketplace add MARKETPLACE_REPO"
- Presentations: 1800x900
**Themes:** # In build script
- Documentation: Nord, GitHub Dark sed "s|MARKETPLACE_REPO|$DETECTED_REPO|g" template.tape > rendered.tape
- Code demos: Dracula, Monokai vhs rendered.tape
- Presentations: High-contrast themes ```
**Duration:** ## Timing & Sizing Reference
- Target: 15-30 seconds
- Maximum: 60 seconds | Context | Width | Height | Font | Duration |
- Create series for complex topics |---------|-------|--------|------|----------|
| README/docs | 1400 | 600 | 16-20 | 10-20s |
| Presentation | 1800 | 900 | 24 | 15-30s |
| Compact embed | 1200 | 600 | 14-16 | 10-15s |
| Wide output | 1600 | 800 | 16 | 15-30s |
See `references/best_practices.md` for detailed guidelines.
## Troubleshooting ## Troubleshooting
### VHS Not Installed | Problem | Solution |
|---------|----------|
```bash | VHS not installed | `brew install charmbracelet/tap/vhs` |
# macOS | gifsicle not installed | `brew install gifsicle` |
brew install vhs | GIF too large | Reduce dimensions, sleep times, or use `--speed 2` |
| Text wraps/breaks | Increase `--width` or decrease `--font-size` |
# Linux (via Go) | VHS parse error on `$` or `\"` | Use base64 encoding (see Critical section above) |
go install github.com/charmbracelet/vhs@latest | Hidden commands leak into GIF | Add `clear` + `Sleep 500ms` before `Show` |
``` | Commands execute before previous finishes | Increase `Sleep` duration |
### Asciinema Not Installed
```bash
# macOS
brew install asciinema
# Linux
sudo apt install asciinema
```
### Demo File Too Large
**Solutions:**
1. Reduce duration (shorter sleep times)
2. Use smaller dimensions (1200x600)
3. Consider MP4 format: `Output demo.mp4`
4. Split into multiple shorter demos
### Output Not Readable
**Solutions:**
1. Increase font size: `--font-size 18`
2. Use wider terminal: `--width 1600`
3. Choose high-contrast theme: `--theme "Dracula"`
4. Test on target display device
## Examples
### Example 1: Quick Install Demo
User request: "Create a demo showing npm install"
```bash
scripts/auto_generate_demo.py \
-c "npm install my-package" \
-o install-demo.gif \
--title "Package Installation"
```
### Example 2: Multi-Step Tutorial
User request: "Create a demo showing project setup with git clone, install, and run"
```bash
scripts/auto_generate_demo.py \
-c "git clone https://github.com/user/repo.git" \
-c "cd repo" \
-c "npm install" \
-c "npm start" \
-o setup-demo.gif \
--title "Project Setup" \
--theme "Nord"
```
### Example 3: Batch Generation
User request: "Generate demos for all my CLI tool features"
1. Create `features-demos.yaml`:
```yaml
demos:
- name: "Help Command"
output: "help.gif"
commands: ["my-tool --help"]
- name: "Init Command"
output: "init.gif"
commands: ["my-tool init", "ls -la"]
- name: "Run Command"
output: "run.gif"
commands: ["my-tool run --verbose"]
```
2. Generate all:
```bash
scripts/batch_generate.py features-demos.yaml --output-dir ./demos
```
### Example 4: Interactive Session
User request: "Record me using my CLI tool interactively"
```bash
scripts/record_interactive.sh my-session.gif --theme "Tokyo Night"
# User types commands naturally
# Ctrl+D to finish
```
## Bundled Resources
### scripts/
- **`auto_generate_demo.py`** - Automated demo generation from command lists
- **`batch_generate.py`** - Generate multiple demos from YAML/JSON config
- **`record_interactive.sh`** - Record and convert interactive terminal sessions
### references/
- **`vhs_syntax.md`** - Complete VHS tape file syntax reference
- **`best_practices.md`** - Demo creation guidelines and best practices
### assets/
- **`templates/basic.tape`** - Basic command demo template
- **`templates/interactive.tape`** - Interactive typing demo template
- **`examples/batch-config.yaml`** - Example batch configuration file
## Dependencies ## Dependencies
**Required:** **Required:** VHS (`brew install charmbracelet/tap/vhs`)
- VHS (https://github.com/charmbracelet/vhs)
**Optional:** **Optional:** gifsicle (speed-up), asciinema (interactive recording), ffmpeg (frame verification), PyYAML (batch YAML configs)
- asciinema (for interactive recording)
- PyYAML (for batch YAML configs): `pip install pyyaml`
## Output Formats
VHS supports multiple output formats:
```tape
Output demo.gif # GIF (default, best for documentation)
Output demo.mp4 # MP4 (better compression for long demos)
Output demo.webm # WebM (smaller file size)
```
Choose based on use case:
- **GIF**: Documentation, README files, easy embedding
- **MP4**: Longer demos, better quality, smaller size
- **WebM**: Web-optimized, smallest file size
## Summary
This skill provides three main approaches:
1. **Automated** (`auto_generate_demo.py`) - Quick, easy, smart defaults
2. **Batch** (`batch_generate.py`) - Multiple demos, consistent settings
3. **Interactive** (`record_interactive.sh`) - Live recording, real output
Choose the approach that best fits the user's needs. For most cases, automated generation is the fastest and most convenient option.

View File

@@ -0,0 +1,51 @@
# Self-bootstrapping demo template
# Cleans previous state, sets up environment, records clean demo
#
# MARKETPLACE_REPO — replaced by recording script via sed
# BASE64_WRAPPER — base64-encoded output filter function
# To create: echo 'my_func() { command my_func "$@" 2>&1 | grep -v "noise"; }' | base64
Output demo.gif
Set Theme "Catppuccin Latte"
Set FontSize 24
Set Width 1400
Set Height 600
Set Padding 20
Set TypingSpeed 10ms
Set Shell zsh
# Hidden bootstrap: cleanup + output filter + clear screen
Hide
Type "cleanup-command-here 2>/dev/null"
Enter
Sleep 3s
# Base64-encoded wrapper filters noisy output lines.
# VHS cannot parse shell special chars ($, \") in Type strings, so base64 is the workaround.
Type "echo BASE64_WRAPPER | base64 -d > /tmp/cw.sh && source /tmp/cw.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show
# Stage 1: Setup
Type "setup-command MARKETPLACE_REPO"
Enter
Sleep 8s
Enter
Sleep 300ms
# Stage 2: Main action
Type "main-command"
Enter
Sleep 3s
Enter
Sleep 300ms
# Stage 3: Verify
Type "verify-command"
Enter
Sleep 2s
Sleep 1s

View File

@@ -0,0 +1,240 @@
# Advanced VHS Demo Patterns
Battle-tested patterns from production demo recording workflows.
## Contents
- Self-bootstrapping tapes
- Output noise filtering with base64 wrapper
- Frame-level verification
- Post-processing with gifsicle
- Auto-detection and template rendering
- Recording script structure
## Self-Bootstrapping Tapes
A self-bootstrapping tape cleans its own state before recording, so running it twice produces identical output. Three phases:
```tape
# Phase 1: HIDDEN CLEANUP — remove previous state
Hide
Type "my-tool uninstall 2>/dev/null; my-tool reset 2>/dev/null"
Enter
Sleep 3s
# Phase 2: HIDDEN SETUP — create helpers (base64 for special chars)
Type "echo <base64-wrapper> | base64 -d > /tmp/helper.sh && source /tmp/helper.sh"
Enter
Sleep 500ms
# Phase 3: CLEAR + SHOW — wipe buffer before revealing
Type "clear"
Enter
Sleep 500ms
Show
# Phase 4: VISIBLE DEMO — what the viewer sees
Type "my-tool install"
Enter
Sleep 3s
```
**Why `clear` before `Show`:** VHS's `Hide` stops recording frames, but the terminal buffer still accumulates text. Without `clear`, the hidden commands' text appears in the first visible frame.
## Output Noise Filtering with Base64
Many CLI tools produce verbose progress output that clutters demos. The solution: a hidden shell wrapper that filters noise lines.
### Step 1: Create the wrapper function
```bash
# The function you want (can't type directly in VHS due to $/" chars)
my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }
```
### Step 2: Base64 encode it
```bash
echo 'my_tool() { command my_tool "$@" 2>&1 | grep -v -E "cache|progress|downloading|timeout"; }' | base64
# Output: bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K
```
### Step 3: Use in tape
```tape
Hide
Type "echo bXlfdG9vbCgpIHsgY29tbWFuZC4uLn0K | base64 -d > /tmp/w.sh && source /tmp/w.sh"
Enter
Sleep 500ms
Type "clear"
Enter
Sleep 500ms
Show
# Now `my_tool` calls the wrapper — clean output
Type "my_tool deploy"
Enter
Sleep 5s
```
### When to filter
- Git operations: filter "Cloning", "Refreshing", cache messages
- Package managers: filter download progress, cache hits
- Build tools: filter intermediate compilation steps
- Any command with `SSH not configured`, `timeout: 120s`, etc.
## Frame-Level Verification
After recording, extract and inspect key frames to verify the GIF shows what you expect.
### Extract specific frames
```bash
# Frame at position N (0-indexed)
ffmpeg -i demo.gif -vf "select=eq(n\,100)" -frames:v 1 /tmp/frame_100.png -y 2>/dev/null
# Multiple frames at once
for n in 50 200 400; do
ffmpeg -i demo.gif -vf "select=eq(n\,$n)" -frames:v 1 "/tmp/frame_$n.png" -y 2>/dev/null
done
```
### Check total frame count and duration
```bash
ffmpeg -i demo.gif 2>&1 | grep -E "Duration|fps"
# Duration: 00:00:10.50, ... 25 fps → 262 frames total
```
### What to verify
| Frame | Check |
|-------|-------|
| First (~frame 5) | No leaked hidden commands |
| Mid (~frame N/2) | Key output visible, no noise |
| Final (~frame N-10) | All commands completed, result shown |
### Claude can read frames
Use the Read tool on extracted PNG files — Claude's vision can verify text content in terminal screenshots.
## Post-Processing with gifsicle
Speed up or optimize GIFs after recording, avoiding re-recording.
### Speed control
```bash
# 2x speed — halve frame delay (most common)
gifsicle -d2 input.gif "#0-" > output.gif
# 1.5x speed
gifsicle -d4 input.gif "#0-" > output.gif
# 3x speed
gifsicle -d1 input.gif "#0-" > output.gif
```
### Optimize file size
```bash
# Lossless optimization
gifsicle -O3 input.gif > optimized.gif
# Reduce colors (lossy but smaller)
gifsicle --colors 128 input.gif > smaller.gif
```
### Typical recording script pattern
```bash
# Record at normal speed
vhs demo.tape
# Speed up 2x for final output
cp demo.gif /tmp/demo_raw.gif
gifsicle -d2 /tmp/demo_raw.gif "#0-" > demo.gif
rm /tmp/demo_raw.gif
```
## Auto-Detection and Template Rendering
For demos that need to adapt to the environment (e.g., different repo URLs, detected tools).
### Template placeholders
Use `sed` to replace placeholders before recording:
```tape
# demo.tape (template)
Type "tool marketplace add REPO_PLACEHOLDER"
Enter
```
```bash
# Build script detects the correct repo
REPO=$(detect_repo)
sed "s|REPO_PLACEHOLDER|$REPO|g" demo.tape > /tmp/rendered.tape
vhs /tmp/rendered.tape
```
### Auto-detect pattern (shell function)
```bash
detect_repo() {
local upstream origin
upstream=$(git remote get-url upstream 2>/dev/null | sed 's|.*github.com[:/]||; s|\.git$||') || true
origin=$(git remote get-url origin 2>/dev/null | sed 's|.*github.com[:/]||; s|\.git$||') || true
# Check upstream first (canonical), then origin (fork)
if [[ -n "$upstream" ]] && gh api "repos/$upstream/contents/.target-file" &>/dev/null; then
echo "$upstream"
elif [[ -n "$origin" ]]; then
echo "$origin"
else
echo "fallback/default"
fi
}
```
## Recording Script Structure
A complete recording script follows this pattern:
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# 1. Check prerequisites
for cmd in vhs gifsicle; do
command -v "$cmd" &>/dev/null || { echo "Missing: $cmd"; exit 1; }
done
# 2. Auto-detect dynamic values
REPO=$(detect_repo)
echo "Using repo: $REPO"
# 3. Render tape template
sed "s|PLACEHOLDER|$REPO|g" "$SCRIPT_DIR/demo.tape" > /tmp/rendered.tape
# 4. Clean previous state
cleanup_state || true
# 5. Record
(cd "$REPO_DIR" && vhs /tmp/rendered.tape)
# 6. Speed up
cp "$REPO_DIR/demo.gif" /tmp/raw.gif
gifsicle -d2 /tmp/raw.gif "#0-" > "$REPO_DIR/demo.gif"
# 7. Clean up
cleanup_state || true
rm -f /tmp/raw.gif /tmp/rendered.tape
# 8. Report
SIZE=$(ls -lh "$REPO_DIR/demo.gif" | awk '{print $5}')
echo "Done: demo.gif ($SIZE)"
```

View File

@@ -2,10 +2,14 @@
""" """
Auto-generate CLI demos from command descriptions. Auto-generate CLI demos from command descriptions.
This script creates VHS tape files and generates GIF demos automatically. Creates VHS tape files and generates GIF demos with support for:
- Hidden bootstrap commands (self-cleaning state)
- Output noise filtering via base64-encoded wrapper
- Post-processing speed-up via gifsicle
""" """
import argparse import argparse
import base64
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
@@ -21,21 +25,54 @@ def create_tape_file(
width: int = 1400, width: int = 1400,
height: int = 700, height: int = 700,
padding: int = 20, padding: int = 20,
bootstrap: Optional[List[str]] = None,
filter_pattern: Optional[str] = None,
) -> str: ) -> str:
"""Generate a VHS tape file from commands.""" """Generate a VHS tape file from commands."""
tape_lines = [ tape_lines = [
f'Output {output_gif}', f'Output {output_gif}',
'', f'Set Theme "{theme}"',
f'Set FontSize {font_size}', f'Set FontSize {font_size}',
f'Set Width {width}', f'Set Width {width}',
f'Set Height {height}', f'Set Height {height}',
f'Set Theme "{theme}"',
f'Set Padding {padding}', f'Set Padding {padding}',
'Set TypingSpeed 10ms',
'Set Shell zsh',
'', '',
] ]
# Add title if provided # Hidden bootstrap: cleanup + optional output filter
has_hidden = bootstrap or filter_pattern
if has_hidden:
tape_lines.append('Hide')
if bootstrap:
# Combine all bootstrap commands with semicolons
combined = "; ".join(
cmd if "2>/dev/null" in cmd else f"{cmd} 2>/dev/null"
for cmd in bootstrap
)
tape_lines.append(f'Type "{combined}"')
tape_lines.append('Enter')
tape_lines.append('Sleep 3s')
if filter_pattern:
# Create a wrapper function that filters noisy output
wrapper = f'_wrap() {{ "$@" 2>&1 | grep -v -E "{filter_pattern}"; }}'
encoded = base64.b64encode(wrapper.encode()).decode()
tape_lines.append(f'Type "echo {encoded} | base64 -d > /tmp/cw.sh && source /tmp/cw.sh"')
tape_lines.append('Enter')
tape_lines.append('Sleep 500ms')
# Clear screen before Show to prevent hidden text from leaking
tape_lines.append('Type "clear"')
tape_lines.append('Enter')
tape_lines.append('Sleep 500ms')
tape_lines.append('Show')
tape_lines.append('')
# Title
if title: if title:
tape_lines.extend([ tape_lines.extend([
f'Type "# {title}" Sleep 500ms Enter', f'Type "# {title}" Sleep 500ms Enter',
@@ -43,68 +80,94 @@ def create_tape_file(
'', '',
]) ])
# Add commands with smart timing # Commands with smart timing
for i, cmd in enumerate(commands, 1): for i, cmd in enumerate(commands, 1):
# Type the command # If filter is active, prefix with _wrap
tape_lines.append(f'Type "{cmd}" Sleep 500ms') if filter_pattern:
tape_lines.append(f'Type "_wrap {cmd}"')
else:
tape_lines.append(f'Type "{cmd}"')
tape_lines.append('Enter') tape_lines.append('Enter')
# Smart sleep based on command complexity # Smart sleep based on command complexity
if any(keyword in cmd.lower() for keyword in ['install', 'build', 'test', 'deploy']): if any(kw in cmd.lower() for kw in ['install', 'build', 'test', 'deploy', 'marketplace']):
sleep_time = '3s' sleep_time = '3s'
elif any(keyword in cmd.lower() for keyword in ['ls', 'pwd', 'echo', 'cat']): elif any(kw in cmd.lower() for kw in ['ls', 'pwd', 'echo', 'cat', 'grep']):
sleep_time = '1s' sleep_time = '1s'
else: else:
sleep_time = '2s' sleep_time = '2s'
tape_lines.append(f'Sleep {sleep_time}') tape_lines.append(f'Sleep {sleep_time}')
# Add spacing between commands # Empty line between stages for readability
if i < len(commands): if i < len(commands):
tape_lines.append('Enter')
tape_lines.append('Sleep 300ms')
tape_lines.append('') tape_lines.append('')
tape_lines.append('')
tape_lines.append('Sleep 1s')
return '\n'.join(tape_lines) return '\n'.join(tape_lines)
def speed_up_gif(gif_path: str, speed: int) -> bool:
"""Speed up GIF using gifsicle. Returns True on success."""
try:
subprocess.run(['gifsicle', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("⚠ gifsicle not found, skipping speed-up. Install: brew install gifsicle", file=sys.stderr)
return False
# delay = 10 / speed (10 = normal, 5 = 2x, 3 = ~3x)
delay = max(1, 10 // speed)
tmp = f"/tmp/demo_raw_{Path(gif_path).stem}.gif"
subprocess.run(['cp', gif_path, tmp], check=True)
with open(gif_path, 'wb') as out:
subprocess.run(['gifsicle', f'-d{delay}', tmp, '#0-'], stdout=out, check=True)
Path(tmp).unlink(missing_ok=True)
return True
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Auto-generate CLI demos from commands', description='Auto-generate CLI demos from commands',
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=''' epilog='''
Examples: Examples:
# Generate demo from single command # Simple demo
%(prog)s -c "npm install" -o demo.gif %(prog)s -c "npm install" -o demo.gif
# Generate demo with multiple commands # With hidden bootstrap (self-cleaning)
%(prog)s -c "git clone repo" -c "cd repo" -c "npm install" -o setup.gif %(prog)s -c "my-tool run" -o demo.gif \\
--bootstrap "my-tool reset" --speed 2
# Custom theme and size # With output noise filtering
%(prog)s -c "ls -la" -o demo.gif --theme Monokai --width 1200 %(prog)s -c "deploy-tool push" -o demo.gif \\
--filter "cache|progress|downloading"
# With title
%(prog)s -c "echo Hello" -o demo.gif --title "My Demo"
''' '''
) )
parser.add_argument('-c', '--command', action='append', required=True, parser.add_argument('-c', '--command', action='append', required=True,
help='Command to include in demo (can be specified multiple times)') help='Command to include (repeatable)')
parser.add_argument('-o', '--output', required=True, parser.add_argument('-o', '--output', required=True,
help='Output GIF file path') help='Output GIF file path')
parser.add_argument('--title', help='Demo title (optional)') parser.add_argument('--title', help='Demo title')
parser.add_argument('--theme', default='Dracula', parser.add_argument('--theme', default='Dracula', help='VHS theme (default: Dracula)')
help='VHS theme (default: Dracula)') parser.add_argument('--font-size', type=int, default=16, help='Font size (default: 16)')
parser.add_argument('--font-size', type=int, default=16, parser.add_argument('--width', type=int, default=1400, help='Terminal width (default: 1400)')
help='Font size (default: 16)') parser.add_argument('--height', type=int, default=700, help='Terminal height (default: 700)')
parser.add_argument('--width', type=int, default=1400, parser.add_argument('--bootstrap', action='append',
help='Terminal width (default: 1400)') help='Hidden setup command run before demo (repeatable)')
parser.add_argument('--height', type=int, default=700, parser.add_argument('--filter',
help='Terminal height (default: 700)') help='Regex pattern to filter from command output')
parser.add_argument('--speed', type=int, default=1,
help='Playback speed multiplier (default: 1, uses gifsicle)')
parser.add_argument('--no-execute', action='store_true', parser.add_argument('--no-execute', action='store_true',
help='Generate tape file only, do not execute VHS') help='Generate tape file only')
args = parser.parse_args() args = parser.parse_args()
# Generate tape file content
tape_content = create_tape_file( tape_content = create_tape_file(
commands=args.command, commands=args.command,
output_gif=args.output, output_gif=args.output,
@@ -113,37 +176,38 @@ Examples:
font_size=args.font_size, font_size=args.font_size,
width=args.width, width=args.width,
height=args.height, height=args.height,
bootstrap=args.bootstrap,
filter_pattern=args.filter,
) )
# Write tape file
output_path = Path(args.output) output_path = Path(args.output)
tape_file = output_path.with_suffix('.tape') tape_file = output_path.with_suffix('.tape')
tape_file.write_text(tape_content)
with open(tape_file, 'w') as f:
f.write(tape_content)
print(f"✓ Generated tape file: {tape_file}") print(f"✓ Generated tape file: {tape_file}")
if not args.no_execute: if not args.no_execute:
# Check if VHS is installed
try: try:
subprocess.run(['vhs', '--version'], capture_output=True, check=True) subprocess.run(['vhs', '--version'], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError): except (subprocess.CalledProcessError, FileNotFoundError):
print("✗ VHS is not installed!", file=sys.stderr) print("✗ VHS not installed. Install: brew install charmbracelet/tap/vhs", file=sys.stderr)
print("Install it with: brew install vhs", file=sys.stderr) print(f"✓ Run manually: vhs {tape_file}", file=sys.stderr)
print(f"✓ You can manually run: vhs < {tape_file}", file=sys.stderr)
return 1 return 1
# Execute VHS print(f"Recording: {args.output}")
print(f"Generating GIF: {args.output}")
try: try:
subprocess.run(['vhs', str(tape_file)], check=True) subprocess.run(['vhs', str(tape_file)], check=True)
print(f"✓ Demo generated: {args.output}")
print(f" Size: {output_path.stat().st_size / 1024:.1f} KB")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"✗ VHS execution failed: {e}", file=sys.stderr) print(f"✗ VHS failed: {e}", file=sys.stderr)
return 1 return 1
# Post-processing speed-up
if args.speed > 1:
print(f"Speeding up {args.speed}x...")
speed_up_gif(args.output, args.speed)
size_kb = output_path.stat().st_size / 1024
print(f"✓ Done: {args.output} ({size_kb:.0f} KB)")
return 0 return 0

274
marketplace-dev/SKILL.md Normal file
View File

@@ -0,0 +1,274 @@
---
name: marketplace-dev
description: |
Converts any Claude Code skills repository into an official plugin marketplace.
Analyzes existing skills, generates .claude-plugin/marketplace.json conforming to
the Anthropic spec, validates with `claude plugin validate`, tests real installation,
and creates a PR to the upstream repo. Encodes hard-won anti-patterns from real
marketplace development (schema traps, version semantics, description pitfalls).
Use when the user mentions: marketplace, plugin support, one-click install,
marketplace.json, plugin distribution, auto-update, or wants a skills repo
installable via `claude plugin install`. Also trigger when the user has a skills
repo and asks about packaging, distribution, or making it installable.
argument-hint: [repo-path]
---
# marketplace-dev
Convert a Claude Code skills repository into an official plugin marketplace so users
can install skills via `claude plugin marketplace add` and get auto-updates.
**Input**: a repo with `skills/` directories containing SKILL.md files.
**Output**: `.claude-plugin/marketplace.json` + validated + installation-tested + PR-ready.
## Phase 1: Analyze the Target Repo
### Step 1: Discover all skills
```bash
# Find every SKILL.md
find <repo-path>/skills -name "SKILL.md" -type f 2>/dev/null
```
For each skill, extract from SKILL.md frontmatter:
- `name` — the skill identifier
- `description` — the ORIGINAL text, do NOT rewrite or translate
### Step 2: Read the repo metadata
- `VERSION` file (if exists) — this becomes `metadata.version`
- `README.md` — understand the project, author info, categories
- `LICENSE` — note the license type
- Git remotes — identify upstream vs fork (`git remote -v`)
### Step 3: Determine categories
Group skills by function. Categories are freeform strings. Good patterns:
- `business-diagnostics`, `content-creation`, `thinking-tools`, `utilities`
- `developer-tools`, `productivity`, `documentation`, `security`
Ask the user to confirm categories if grouping is ambiguous.
## Phase 2: Create marketplace.json
### The official schema (memorize this)
Read `references/marketplace_schema.md` for the complete field reference.
Key rules that are NOT obvious from the docs:
1. **`$schema` field is REJECTED** by `claude plugin validate`. Do not include it.
2. **`metadata` only has 3 valid fields**: `description`, `version`, `pluginRoot`. Nothing else.
`metadata.homepage` does NOT exist — the validator accepts it silently but it's not in the spec.
3. **`metadata.version`** is the marketplace catalog version, NOT individual plugin versions.
It should match the repo's VERSION file (e.g., `"2.3.0"`).
4. **Plugin entry `version`** is independent. For first-time marketplace registration, use `"1.0.0"`.
5. **`strict: false`** is required when there's no `plugin.json` in the repo.
With `strict: false`, the marketplace entry IS the entire plugin definition.
Having BOTH `strict: false` AND a `plugin.json` with components causes a load failure.
6. **`source: "./"` with `skills: ["./skills/<name>"]`** is the pattern for skills in the same repo.
7. **Reserved marketplace names** that CANNOT be used: `claude-code-marketplace`,
`claude-code-plugins`, `claude-plugins-official`, `anthropic-marketplace`,
`anthropic-plugins`, `agent-skills`, `knowledge-work-plugins`, `life-sciences`.
8. **`tags` vs `keywords`**: Both are optional. In the current Claude Code source,
`keywords` is defined but never consumed in search. `tags` only has a UI effect
for the value `"community-managed"` (shows a label). Neither affects discovery.
The Discover tab searches only `name` + `description` + `marketplaceName`.
Include `keywords` for future-proofing but don't over-invest.
### Generate the marketplace.json
Use this template, filling in from the analysis:
```json
{
"name": "<marketplace-name>",
"owner": {
"name": "<github-org-or-username>"
},
"metadata": {
"description": "<one-line description of the marketplace>",
"version": "<from-VERSION-file-or-1.0.0>"
},
"plugins": [
{
"name": "<skill-name>",
"description": "<EXACT text from SKILL.md frontmatter, do NOT rewrite>",
"source": "./",
"strict": false,
"version": "1.0.0",
"category": "<category>",
"keywords": ["<relevant>", "<keywords>"],
"skills": ["./skills/<skill-name>"]
}
]
}
```
### Naming the marketplace
The `name` field is what users type after `@` in install commands:
`claude plugin install dbs@<marketplace-name>`
Choose a name that is:
- Short and memorable
- kebab-case (lowercase, hyphens only)
- Related to the project identity, not generic
### Description rules
- **Use the ORIGINAL description from each SKILL.md frontmatter**
- Do NOT translate, embellish, or "improve" descriptions
- If the repo's audience is Chinese, keep descriptions in Chinese
- If bilingual, use the first language in the SKILL.md description field
- The `metadata.description` at marketplace level can be a new summary
## Maintaining an existing marketplace
When adding a new plugin to an existing marketplace.json:
1. **Bump `metadata.version`** — this is the marketplace catalog version.
Follow semver: new plugin = minor bump, breaking change = major bump.
2. **Update `metadata.description`** — append the new skill's summary.
3. **Set new plugin `version` to `"1.0.0"`** — it's new to the marketplace.
4. **Bump existing plugin `version`** when its SKILL.md content changes.
Claude Code uses version to detect updates — same version = skip update.
5. **Audit `metadata` for invalid fields**`metadata.homepage` is a common
mistake (not in spec, silently ignored). Remove if found.
## Phase 3: Validate
### Step 1: CLI validation
```bash
claude plugin validate .
```
This catches schema errors. Common failures and fixes:
- `Unrecognized key: "$schema"` → remove the `$schema` field
- `Duplicate plugin name` → ensure all names are unique
- `Path contains ".."` → use `./` relative paths only
### Step 2: Installation test
```bash
# Add as local marketplace
claude plugin marketplace add .
# Install a plugin
claude plugin install <plugin-name>@<marketplace-name>
# Verify it appears
claude plugin list | grep <plugin-name>
# Check for updates (should say "already at latest")
claude plugin update <plugin-name>@<marketplace-name>
# Clean up
claude plugin uninstall <plugin-name>@<marketplace-name>
claude plugin marketplace remove <marketplace-name>
```
### Step 3: GitHub installation test (if pushed)
```bash
# Test from GitHub (requires the branch to be pushed)
claude plugin marketplace add <github-user>/<repo>
claude plugin install <plugin-name>@<marketplace-name>
# Verify
claude plugin list | grep <plugin-name>
# Clean up
claude plugin uninstall <plugin-name>@<marketplace-name>
claude plugin marketplace remove <marketplace-name>
```
## Pre-flight Checklist (MUST pass before proceeding to PR)
Run this checklist after every marketplace.json change. Do not skip items.
### Sync check: skills ↔ marketplace.json
```bash
# List all skill directories on disk
DISK_SKILLS=$(find skills -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | sort)
# List all skills registered in marketplace.json
JSON_SKILLS=$(python3 -c "
import json
with open('.claude-plugin/marketplace.json') as f:
data = json.load(f)
for p in data['plugins']:
for s in p.get('skills', []):
print(s.split('/')[-1])
" | sort)
# Compare — must match
diff <(echo "$DISK_SKILLS") <(echo "$JSON_SKILLS")
```
If diff shows output, skills are out of sync. Fix before proceeding.
### Metadata check
Verify these by reading marketplace.json:
- [ ] `metadata.version` bumped from previous version
- [ ] `metadata.description` mentions all skill categories
- [ ] No `metadata.homepage` (not in spec, silently ignored)
- [ ] No `$schema` field (rejected by validator)
### Per-plugin check
For each plugin entry:
- [ ] `description` matches SKILL.md frontmatter EXACTLY (not rewritten)
- [ ] `version` is `"1.0.0"` for new plugins, bumped for changed plugins
- [ ] `source` is `"./"` and `skills` path starts with `"./"`
- [ ] `strict` is `false` (no plugin.json in repo)
- [ ] `name` is kebab-case, unique across all entries
### Final validation
```bash
claude plugin validate .
```
Must show `✔ Validation passed` before creating PR.
## Phase 4: Create PR
### Principles
- **Pure incremental**: do NOT modify any existing files (skills, README, etc.)
- **Squash commits**: avoid binary bloat in git history from iterative changes
- Only add: `.claude-plugin/marketplace.json`, optionally `scripts/`, optionally update README
### README update (if appropriate)
Add the marketplace install method above existing install instructions:
```markdown
## Install
![demo](demo.gif) <!-- only if demo exists -->
**Claude Code plugin marketplace (one-click install, auto-update):**
\`\`\`bash
claude plugin marketplace add <owner>/<repo>
claude plugin install <skill>@<marketplace-name>
\`\`\`
```
### PR description template
Include:
- What was added (marketplace.json with N skills, M categories)
- Install commands users will use after merge
- Design decisions (pure incremental, original descriptions, etc.)
- Validation evidence (`claude plugin validate .` passed)
- Test plan (install commands to verify)
## Anti-Patterns (things that went wrong and how to fix them)
Read `references/anti_patterns.md` for the full list of pitfalls discovered during
real marketplace development. These are NOT theoretical — every one was encountered
and debugged in production.

View File

@@ -0,0 +1,100 @@
# Anti-Patterns: Marketplace Development Pitfalls
Every item below was encountered during real marketplace development.
Not theoretical — each cost debugging time.
## Schema Errors
### Adding `$schema` field
- **Symptom**: `claude plugin validate` fails with `Unrecognized key: "$schema"`
- **Fix**: Do not include `$schema`. Unlike many JSON schemas, Claude Code rejects it.
- **Why it's tempting**: Other marketplace examples (like daymade/claude-code-skills) include it,
and it works for JSON Schema-aware editors. But the validator is strict.
### Using `metadata.homepage`
- **Symptom**: Silently ignored — no error, no effect.
- **Fix**: `metadata` only supports `description`, `version`, `pluginRoot`. Put `homepage`
on individual plugin entries if needed.
- **Why it's tricky**: `homepage` IS valid on plugin entries (from plugin.json schema),
so it looks correct but is wrong at the metadata level.
### Conflicting `strict: false` with `plugin.json`
- **Symptom**: Plugin fails to load with "conflicting manifests" error.
- **Fix**: Choose one authority. `strict: false` means marketplace.json is the SOLE
definition. Remove plugin.json or set `strict: true` and let plugin.json define components.
## Version Confusion
### Using marketplace version as plugin version
- **Symptom**: All plugins show the same version (e.g., "2.3.0") even though each was
introduced at different times.
- **Fix**: `metadata.version` = marketplace catalog version (matches repo VERSION file).
Each plugin entry `version` is independent. First time in marketplace = `"1.0.0"`.
### Not bumping version after changes
- **Symptom**: Users don't receive updates after you push changes.
- **Fix**: Claude Code uses version to detect updates. Same version = skip.
Bump the plugin `version` in marketplace.json when you change skill content.
## Description Errors
### Rewriting or translating SKILL.md descriptions
- **Symptom**: Descriptions don't match the actual skill behavior. English descriptions
for a Chinese-audience repo feel foreign.
- **Fix**: Copy the EXACT `description` field from each SKILL.md frontmatter.
The author wrote it for their audience — preserve it.
### Inventing features in descriptions
- **Symptom**: Description promises "8,000+ consultations" or "auto-backup and rollback"
when the SKILL.md doesn't mention these specifics.
- **Fix**: Only state what the SKILL.md frontmatter says. If you want to add context,
use the marketplace-level `metadata.description`, not individual plugin descriptions.
## Installation Testing
### Not testing after GitHub push
- **Symptom**: Local validation passes, but `claude plugin marketplace add <user>/<repo>`
fails because it clones the default branch which doesn't have the marketplace.json.
- **Fix**: Push marketplace.json to the repo's default branch before testing GitHub install.
Feature branches only work if the user specifies the ref.
### Forgetting to clean up test installations
- **Symptom**: Next test run finds stale marketplace/plugins and produces confusing results.
- **Fix**: Always uninstall plugins and remove marketplace after testing:
```bash
claude plugin uninstall <plugin>@<marketplace> 2>/dev/null
claude plugin marketplace remove <marketplace> 2>/dev/null
```
## PR Best Practices
### Modifying existing files unnecessarily
- **Symptom**: PR diff includes unrelated changes (empty lines in .gitignore, whitespace
changes in README), making it harder to review and merge.
- **Fix**: Only add new files. If modifying README, be surgical — only add the install section.
Verify with `git diff upstream/main` that no unrelated files are touched.
### Not squashing commits
- **Symptom**: Git history has 15+ commits with iterative demo.gif changes, bloating the
repo by megabytes. Users cloning the marketplace download all this history.
- **Fix**: Squash all commits into one before creating the PR:
```bash
git reset --soft upstream/main
git commit -m "feat: add Claude Code plugin marketplace"
```
## Discovery Misconceptions
### Over-investing in keywords/tags
- **Symptom**: Spending time crafting perfect keyword lists.
- **Reality**: In the current Claude Code source (verified by reading DiscoverPlugins.tsx),
the Discover tab searches ONLY `name` + `description` + `marketplaceName`.
`keywords` is defined in the schema but never consumed. `tags` only affects UI
for the specific value `"community-managed"`. Include keywords for future-proofing
but don't obsess over them.
### Using `tags` for search optimization
- **Symptom**: Adding `tags: ["business", "diagnosis"]` expecting search improvements.
- **Reality**: Only `tags: ["community-managed"]` has any effect (shows a UI label).
The official Anthropic marketplace (123 plugins) uses tags on only 3 plugins,
all with the value `["community-managed"]`.

View File

@@ -0,0 +1,92 @@
# marketplace.json Complete Schema Reference
Source: https://code.claude.com/docs/en/plugin-marketplaces + https://code.claude.com/docs/en/plugins-reference
Verified against Claude Code source code and `claude plugin validate`.
## Root Level
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | **Yes** | Marketplace identifier, kebab-case. Users see this: `plugin install X@<name>` |
| `owner` | object | **Yes** | `name` (required string), `email` (optional string) |
| `plugins` | array | **Yes** | Array of plugin entries |
| `metadata` | object | No | See metadata fields below |
### metadata fields (ONLY these 3 are valid)
| Field | Type | Description |
|-------|------|-------------|
| `metadata.description` | string | Brief marketplace description |
| `metadata.version` | string | Marketplace catalog version (NOT plugin version) |
| `metadata.pluginRoot` | string | Base directory prepended to relative plugin source paths |
**Invalid metadata fields** (silently ignored or rejected):
- `metadata.homepage` — does NOT exist in spec
- `metadata.repository` — does NOT exist in spec
- `$schema` — REJECTED by `claude plugin validate`
## Plugin Entry Fields
Each entry in the `plugins` array. Can include any field from the plugin.json
manifest schema, plus marketplace-specific fields.
### Required
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Plugin identifier, kebab-case |
| `source` | string or object | Where to fetch the plugin |
### Standard metadata (from plugin.json schema)
| Field | Type | Description |
|-------|------|-------------|
| `description` | string | Brief plugin description |
| `version` | string | Plugin version (independent of metadata.version) |
| `author` | object | `name` (required), `email` (optional), `url` (optional) |
| `homepage` | string | Plugin homepage URL |
| `repository` | string | Source code URL |
| `license` | string | SPDX identifier (MIT, Apache-2.0, etc.) |
| `keywords` | array | Tags for discovery (currently unused in search) |
### Marketplace-specific
| Field | Type | Description |
|-------|------|-------------|
| `category` | string | Freeform category for organization |
| `tags` | array | Tags for searchability (only `community-managed` has UI effect) |
| `strict` | boolean | Default: true. Set false when no plugin.json exists |
### Component paths (from plugin.json schema)
| Field | Type | Description |
|-------|------|-------------|
| `commands` | string or array | Custom command file/directory paths |
| `agents` | string or array | Custom agent file paths |
| `skills` | string or array | Custom skill directory paths |
| `hooks` | string or object | Hook configuration or path |
| `mcpServers` | string or object | MCP server config or path |
| `lspServers` | string or object | LSP server config or path |
| `outputStyles` | string or array | Output style paths |
| `userConfig` | object | User-configurable values prompted at enable time |
| `channels` | array | Channel declarations for message injection |
## Source Types
| Source | Format | Example |
|--------|--------|---------|
| Relative path | string `"./"` | `"source": "./"` |
| GitHub | object | `{"source": "github", "repo": "owner/repo"}` |
| Git URL | object | `{"source": "url", "url": "https://..."}` |
| Git subdirectory | object | `{"source": "git-subdir", "url": "...", "path": "..."}` |
| npm | object | `{"source": "npm", "package": "@scope/pkg"}` |
All object sources support optional `ref` (branch/tag) and `sha` (40-char commit).
## Reserved Marketplace Names
Cannot be used: `claude-code-marketplace`, `claude-code-plugins`,
`claude-plugins-official`, `anthropic-marketplace`, `anthropic-plugins`,
`agent-skills`, `knowledge-work-plugins`, `life-sciences`.
Names impersonating official marketplaces are also blocked.

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# PostToolUse hook: warn when SKILL.md is edited but marketplace.json version not bumped
# Detects skill content changes that need a corresponding version bump in marketplace.json
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
print(data.get('tool_input', {}).get('file_path', ''))
except:
print('')
" 2>/dev/null)
# Only care about SKILL.md edits
[[ "$FILE_PATH" != *"SKILL.md"* ]] && exit 0
# Find the skill name from path (e.g., /repo/skills/dbs-hook/SKILL.md → dbs-hook)
SKILL_DIR=$(dirname "$FILE_PATH")
SKILL_NAME=$(basename "$SKILL_DIR")
# Search for marketplace.json upward from the edited file
SEARCH_DIR="$SKILL_DIR"
MARKETPLACE_JSON=""
while [[ "$SEARCH_DIR" != "/" ]]; do
if [[ -f "$SEARCH_DIR/.claude-plugin/marketplace.json" ]]; then
MARKETPLACE_JSON="$SEARCH_DIR/.claude-plugin/marketplace.json"
break
fi
SEARCH_DIR=$(dirname "$SEARCH_DIR")
done
[[ -z "$MARKETPLACE_JSON" ]] && exit 0
# Check if this skill is registered and if version needs bumping
python3 -c "
import json, sys
with open('$MARKETPLACE_JSON') as f:
data = json.load(f)
skill_name = '$SKILL_NAME'
found = False
for p in data.get('plugins', []):
skills = p.get('skills', [])
for s in skills:
if s.rstrip('/').split('/')[-1] == skill_name:
found = True
version = p.get('version', 'unknown')
print(json.dumps({
'result': f'SKILL.md for \"{skill_name}\" was modified. Remember to bump its version in marketplace.json (currently {version}). Users on the old version won\\'t receive this update otherwise.'
}))
break
if found:
break
if not found:
print(json.dumps({
'result': f'SKILL.md for \"{skill_name}\" was modified but this skill is NOT registered in marketplace.json. Add it if you want it installable via plugin marketplace.'
}))
" 2>/dev/null

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# PostToolUse hook: auto-validate marketplace.json after Write/Edit
# Checks if the edited file is marketplace.json, runs validation if so.
set -euo pipefail
# Read tool use details from stdin
INPUT=$(cat)
# Check if the edited file is marketplace.json
FILE_PATH=$(echo "$INPUT" | python3 -c "
import json, sys
try:
data = json.load(sys.stdin)
path = data.get('tool_input', {}).get('file_path', '')
print(path)
except:
print('')
" 2>/dev/null)
if [[ "$FILE_PATH" == *"marketplace.json"* ]]; then
MARKETPLACE_DIR=$(dirname "$(dirname "$FILE_PATH")")
if [[ -f "$FILE_PATH" ]]; then
RESULT=$(cd "$MARKETPLACE_DIR" && claude plugin validate . 2>&1) || true
if echo "$RESULT" | grep -q "Validation passed"; then
echo '{"result": "marketplace.json validated ✔"}'
else
ERRORS=$(echo "$RESULT" | grep -v "^$" | head -5)
echo "{\"result\": \"marketplace.json validation FAILED:\\n$ERRORS\"}"
fi
fi
fi

View File

@@ -57,4 +57,6 @@ uv run --with weasyprint scripts/batch_convert.py *.md --output-dir ./pdfs
**weasyprint import error**: Run with `uv run --with weasyprint` or use `--backend chrome` instead. **weasyprint import error**: Run with `uv run --with weasyprint` or use `--backend chrome` instead.
**CJK text in code blocks garbled (weasyprint)**: The script auto-detects code blocks containing Chinese/Japanese/Korean characters and converts them to styled divs with CJK-capable fonts. If you still see issues, use `--backend chrome` which has native CJK support. Alternatively, convert code blocks to markdown tables before generating the PDF.
**Chrome header/footer appearing**: The script passes `--no-pdf-header-footer`. If it still appears, your Chrome version may not support this flag — update Chrome. **Chrome header/footer appearing**: The script passes `--no-pdf-header-footer`. If it still appears, your Chrome version may not support this flag — update Chrome.

View File

@@ -125,6 +125,35 @@ def _ensure_list_spacing(text: str) -> str:
return "\n".join(result) return "\n".join(result)
_CJK_RANGE = re.compile(
r"[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff"
r"\U00020000-\U0002a6df\U0002a700-\U0002ebef"
r"\u3000-\u303f\uff00-\uffef]"
)
def _fix_cjk_code_blocks(html: str) -> str:
"""Replace <pre><code> blocks containing CJK with styled divs.
weasyprint renders <pre> blocks using monospace fonts that lack CJK glyphs,
causing garbled output. This converts CJK-heavy code blocks to styled divs
that use the document's CJK font stack instead.
"""
def _replace_if_cjk(match: re.Match) -> str:
content = match.group(1)
if _CJK_RANGE.search(content):
return f'<div class="cjk-code-block">{content}</div>'
return match.group(0)
return re.sub(
r"<pre><code(?:\s[^>]*)?>(.+?)</code></pre>",
_replace_if_cjk,
html,
flags=re.DOTALL,
)
def _md_to_html(md_file: str) -> str: def _md_to_html(md_file: str) -> str:
"""Convert markdown to HTML using pandoc with list spacing preprocessing.""" """Convert markdown to HTML using pandoc with list spacing preprocessing."""
if not shutil.which("pandoc"): if not shutil.which("pandoc"):
@@ -147,7 +176,9 @@ def _md_to_html(md_file: str) -> str:
print(f"Error: pandoc failed: {result.stderr}", file=sys.stderr) print(f"Error: pandoc failed: {result.stderr}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return result.stdout html = result.stdout
html = _fix_cjk_code_blocks(html)
return html
def _build_full_html(html_content: str, css: str, title: str) -> str: def _build_full_html(html_content: str, css: str, title: str) -> str:

View File

@@ -86,3 +86,46 @@ hr {
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
margin: 1.5em 0; margin: 1.5em 0;
} }
code {
font-family: 'Menlo', 'PingFang SC', 'Heiti SC', 'Noto Sans CJK SC', monospace;
background: #f5f5f5;
padding: 1px 4px;
border-radius: 3px;
font-size: 10pt;
}
pre {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px 16px;
margin: 1em 0;
overflow-wrap: break-word;
white-space: pre-wrap;
word-break: break-all;
}
pre code {
font-family: 'Menlo', 'PingFang SC', 'Heiti SC', 'Noto Sans CJK SC', monospace;
background: none;
padding: 0;
border-radius: 0;
font-size: 9pt;
line-height: 1.6;
}
/* CJK code blocks converted to styled divs by preprocessor.
Uses inherit to reuse body's CJK font (weasyprint may not find PingFang SC). */
.cjk-code-block {
font-family: inherit;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 12px 16px;
margin: 1em 0;
font-size: 10pt;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-all;
}

View File

@@ -110,12 +110,48 @@ header, .date {
} }
code { code {
font-family: 'Menlo', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', monospace;
background: #faf5f0; background: #faf5f0;
padding: 1px 4px; padding: 1px 4px;
border-radius: 3px; border-radius: 3px;
font-size: 12px; font-size: 12px;
} }
pre {
background: #faf5f0;
border: 1px solid #e2d6c8;
border-radius: 4px;
padding: 12px 16px;
margin: 10px 0;
overflow-wrap: break-word;
white-space: pre-wrap;
word-break: break-all;
}
pre code {
font-family: 'Menlo', 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', monospace;
background: none;
padding: 0;
border-radius: 0;
font-size: 11px;
line-height: 1.6;
}
/* CJK code blocks converted to styled divs by preprocessor.
Uses inherit to reuse body's CJK font (weasyprint may not resolve all font names). */
.cjk-code-block {
font-family: inherit;
background: #faf5f0;
border: 1px solid #e2d6c8;
border-radius: 4px;
padding: 12px 16px;
margin: 10px 0;
font-size: 12px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-all;
}
strong { strong {
color: #1f1b17; color: #1f1b17;
} }

View File

@@ -17,15 +17,21 @@ All scripts use PEP 723 inline metadata — `uv run` auto-installs dependencies.
# First time: Initialize database # First time: Initialize database
uv run scripts/fix_transcription.py --init uv run scripts/fix_transcription.py --init
# Phase 1: Dictionary corrections (instant, free) # Single file
uv run scripts/fix_transcription.py --input meeting.md --stage 1 uv run scripts/fix_transcription.py --input meeting.md --stage 1
# Batch: multiple files in parallel (use shell loop)
for f in /path/to/*.txt; do
uv run scripts/fix_transcription.py --input "$f" --stage 1
done
``` ```
After Stage 1, Claude reads the output and fixes remaining ASR errors natively (no API key needed): After Stage 1, Claude reads the output and fixes remaining ASR errors natively (no API key needed):
1. Read Stage 1 output in ~200-line chunks using the Read tool 1. Read all Stage 1 outputs — read **entire** transcript before proposing corrections (later context disambiguates earlier errors)
2. Identify ASR errors — homophones, garbled terms, broken sentences 2. Identify ASR errors — compile all corrections across files
3. Present corrections in a table for user review before applying 3. Apply fixes with sed in batch, verify each with diff
4. Save stable patterns to dictionary for future reuse 4. Finalize: rename `_stage1.md``.md`, delete original `.txt`
5. Save stable patterns to dictionary for future reuse
See `references/example_session.md` for a concrete input/output walkthrough. See `references/example_session.md` for a concrete input/output walkthrough.
@@ -51,6 +57,25 @@ Two-phase pipeline with persistent learning:
**After fixing, always save reusable corrections to dictionary.** This is the skill's core value — see `references/iteration_workflow.md` for the complete checklist. **After fixing, always save reusable corrections to dictionary.** This is the skill's core value — see `references/iteration_workflow.md` for the complete checklist.
### Dictionary Addition After Fixing
After native AI correction, review all applied fixes and decide which to save. Use this decision matrix:
| Pattern type | Example | Action |
|-------------|---------|--------|
| Non-word → correct term | 克劳锐→Claude, cloucode→Claude Code | ✅ Add (zero false positive risk) |
| Rare word → correct term | 潜彩→前采, 维星→韦青 | ✅ Add (verify it's not a real word first) |
| Person/company name ASR error | 宋天航→宋天生, 策马攀山→策马看山 | ✅ Add (stable, unique) |
| Common word → context word | 争→蒸, 钱财→前采, 报纸→标品 | ❌ Skip (high false positive risk) |
| Real brand → different brand | Xcode→Claude Code, Clover→Claude | ❌ Skip (real words in other contexts) |
Batch add multiple corrections in one session:
```bash
uv run scripts/fix_transcription.py --add "错误1" "正确1" --domain tech
uv run scripts/fix_transcription.py --add "错误2" "正确2" --domain business
# Chain with && for efficiency
```
## False Positive Prevention ## False Positive Prevention
Adding wrong dictionary rules silently corrupts future transcripts. **Read `references/false_positive_guide.md` before adding any correction rule**, especially for short words (≤2 chars) or common Chinese words that appear correctly in normal text. Adding wrong dictionary rules silently corrupts future transcripts. **Read `references/false_positive_guide.md` before adding any correction rule**, especially for short words (≤2 chars) or common Chinese words that appear correctly in normal text.
@@ -59,21 +84,46 @@ Adding wrong dictionary rules silently corrupts future transcripts. **Read `refe
When running inside Claude Code, use Claude's own language understanding for Phase 2: When running inside Claude Code, use Claude's own language understanding for Phase 2:
1. Run Stage 1 (dictionary): `--input file.md --stage 1` 1. Run Stage 1 (dictionary) on all files (parallel if multiple)
2. Verify Stage 1 — diff original vs output. If dictionary introduced false positives, work from the **original** file 2. Verify Stage 1 — diff original vs output. If dictionary introduced false positives, work from the **original** file
3. Read the full text in ~200-line chunks. Read the entire transcript before proposing corrections — later context often disambiguates earlier errors 3. Read **all** Stage 1 outputs fully before proposing any corrections — later context often disambiguates earlier errors. For large files (>10k tokens), read in chunks but finish the entire file before identifying errors
4. Identify ASR errors: 4. Identify ASR errors per file — classify by confidence:
- Product/tool names: "close code" → "Claude Code", "get Hub" → "GitHub" - **High confidence** (apply directly): non-words, obvious garbling, product name variants
- Technical terms: "Web coding" → "Vibe Coding", "happy pass" → "happy path" - **Medium confidence** (present for review): context-dependent homophones, person names
- Homophone errors: "上海文" → "上下文", "分值" → "分支" 5. Apply fixes efficiently:
- English ASR garbling: "Pre top" → "prototype", "rapper" → "repo" - **Global replacements** (unique non-words like "克劳锐"→"Claude"): use `sed -i ''` with `-e` flags, multiple patterns in one command
- Broken sentences: "很大程。路上" → "很大程度上" - **Context-dependent** (common words like "争"→"蒸" only in distillation context): use sed with longer context phrases for uniqueness, or Edit tool
5. Present corrections in high/medium confidence tables with line numbers 6. Verify with diff: `diff original.txt corrected_stage1.md`
6. Apply with sed on a copy, verify with diff, replace original 7. Finalize files: rename `*_stage1.md``*.md`, delete original `.txt`
7. Generate word diff: `uv run scripts/generate_word_diff.py original.md corrected.md diff.html` 8. Save stable patterns to dictionary (see "Dictionary Addition" below)
8. Save stable patterns to dictionary
9. Remove false positives if Stage 1 had any 9. Remove false positives if Stage 1 had any
### Common ASR Error Patterns
AI product names are frequently garbled. These patterns recur across transcripts:
| Correct term | Common ASR variants |
|-------------|-------------------|
| Claude | cloud, Clou, calloc, 克劳锐, Clover, color |
| Claude Code | cloud code, Xcode, call code, cloucode, cloudcode, color code |
| Claude Agent SDK | cloud agent SDK |
| Opus | Opaas |
| Vibe Coding | web coding, Web coding |
| GitHub | get Hub, Git Hub |
| prototype | Pre top |
Person names and company names also produce consistent ASR errors across sessions — always add confirmed name corrections to the dictionary.
### Efficient Batch Fix Strategy
When fixing multiple files (e.g., 5 transcripts from one day):
1. **Stage 1 in parallel**: run all files through dictionary at once
2. **Read all files first**: build a mental model of speakers, topics, and recurring terms before fixing anything
3. **Compile a global correction list**: many errors repeat across files from the same session (same speakers, same topics)
4. **Apply global corrections first** (sed with multiple `-e` flags), then per-file context-dependent fixes
5. **Verify all diffs**, finalize all files, then do one dictionary addition pass
### Enhanced Capabilities (Native Mode Only) ### Enhanced Capabilities (Native Mode Only)
- **Intelligent paragraph breaks**: Add `\n\n` at logical topic transitions - **Intelligent paragraph breaks**: Add `\n\n` at logical topic transitions

View File

@@ -1,4 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "httpx>=0.24.0",
# "filelock>=3.13.0",
# ]
# ///
""" """
Enhanced transcript fixer wrapper with improved user experience. Enhanced transcript fixer wrapper with improved user experience.

View File

@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Normalize and repair speaker timestamp lines in ASR transcripts. """Normalize and repair speaker timestamp lines in ASR transcripts.
This script targets transcript lines shaped like: This script targets transcript lines shaped like:

View File

@@ -1,4 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "httpx>=0.24.0",
# "filelock>=3.13.0",
# ]
# ///
""" """
Transcript Fixer - Main Entry Point Transcript Fixer - Main Entry Point

View File

@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
""" """
Generate Word-Level Diff HTML Comparison Generate Word-Level Diff HTML Comparison

View File

@@ -1,4 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""Split a transcript into named sections and optionally rebase timestamps. """Split a transcript into named sections and optionally rebase timestamps.
Example: Example:

View File

@@ -64,6 +64,10 @@ COMMON_WORDS_2CHAR: Set[str] = {
"明确", "清晰", "具体", "详细", "准确", "完整", "稳定", "灵活", "明确", "清晰", "具体", "详细", "准确", "完整", "稳定", "灵活",
# --- Domain terms that look like ASR errors but are valid --- # --- Domain terms that look like ASR errors but are valid ---
"线数", "曲线", "分母", "正面", "旗号", "无果", "演技", "线数", "曲线", "分母", "正面", "旗号", "无果", "演技",
# --- Common verb+一 patterns (打一个/来一个/做一下 etc.) ---
# "打一" caused production false positive: "打一个锚" → "答疑个锚" (2026-04)
"打一", "来一", "做一", "写一", "给一", "拉一", "开一", "看一",
"跑一", "找一", "选一", "试一", "走一", "问一", "搞一", "聊一",
} }
# Common 3+ character words that should also be protected. # Common 3+ character words that should also be protected.
@@ -88,6 +92,14 @@ COMMON_WORDS_3PLUS: Set[str] = {
"保健品", "保健操", "医疗保健", "保健品", "保健操", "医疗保健",
"文化内涵", "文化内涵",
"无果而终", "无果而终",
# --- Common verb+一+量词 patterns (防止"打一"→X 类误纠) ---
"打一个", "打一针", "打一下", "打一次", "打一把",
"来一个", "来一下", "来一次", "来一杯",
"做一个", "做一下", "做一次",
"写一个", "写一下", "写一篇",
"给一个", "看一下", "看一看", "看一遍",
"跑一下", "跑一遍", "跑一次",
"试一下", "试一试", "试一次",
# --- Common Chinese idioms/phrases containing short words --- # --- Common Chinese idioms/phrases containing short words ---
# These are needed to prevent idiom corruption # These are needed to prevent idiom corruption
"正面临", "正面对", "正面临", "正面对",
@@ -132,6 +144,8 @@ SUBSTRING_COLLISION_MAP: dict[str, list[str]] = {
"保健": ["保健品", "保健操", "医疗保健"], "保健": ["保健品", "保健操", "医疗保健"],
# "内涵" common in compound words # "内涵" common in compound words
"内涵": ["内涵段子", "文化内涵"], "内涵": ["内涵段子", "文化内涵"],
# "打一" common in verb+一+量词 (2026-04 production false positive)
"打一": ["打一个", "打一针", "打一下", "打一次", "打一把"],
} }
ALL_COMMON_WORDS: Set[str] = COMMON_WORDS_2CHAR | COMMON_WORDS_3PLUS ALL_COMMON_WORDS: Set[str] = COMMON_WORDS_2CHAR | COMMON_WORDS_3PLUS

View File

@@ -1,4 +1,4 @@
Security scan passed Security scan passed
Scanned at: 2026-01-11T15:27:31.794239 Scanned at: 2026-04-06T16:24:46.159857
Tool: gitleaks + pattern-based validation Tool: gitleaks + pattern-based validation
Content hash: 2349813d82b8932f89fdc653c0dde1b2335483478227653554553b59007975c1 Content hash: bfcc70ffa4bfda5e6308942605ad8dc3cf62e8aeade5b00c6d9b774bebb190fb

View File

@@ -1,72 +1,156 @@
--- ---
name: twitter-reader name: twitter-reader
description: Fetch Twitter/X post content by URL using jina.ai API to bypass JavaScript restrictions. Use when Claude needs to retrieve tweet content including author, timestamp, post text, images, and thread replies. Supports individual posts or batch fetching from x.com or twitter.com URLs. description: Fetch Twitter/X post content including long-form Articles with full images and metadata. Use when Claude needs to retrieve tweet/article content, author info, engagement metrics, and embedded media. Supports individual posts and X Articles (long-form content). Automatically downloads all images to local attachments folder and generates complete Markdown with proper image references. Preferred over Jina for X Articles with images.
--- ---
# Twitter Reader # Twitter Reader
Fetch Twitter/X post content without needing JavaScript or authentication. Fetch Twitter/X post and article content with full media support.
## Quick Start (Recommended)
For X Articles with images, use the new fetch_article.py script:
```bash
uv run --with pyyaml python scripts/fetch_article.py <article_url> [output_dir]
```
Example:
```bash
uv run --with pyyaml python scripts/fetch_article.py \
https://x.com/HiTw93/status/2040047268221608281 \
./Clippings
```
This will:
- Fetch structured data via `twitter-cli` (likes, retweets, bookmarks)
- Fetch content with images via `jina.ai` API
- Download all images to `attachments/YYYY-MM-DD-AUTHOR-TITLE/`
- Generate complete Markdown with embedded image references
- Include YAML frontmatter with metadata
### Example Output
```
Fetching: https://x.com/HiTw93/status/2040047268221608281
--------------------------------------------------
Getting metadata...
Title: 你不知道的大模型训练:原理、路径与新实践
Author: Tw93
Likes: 1648
Getting content and images...
Images: 15
Downloading 15 images...
✓ 01-image.jpg
✓ 02-image.jpg
...
✓ Saved: ./Clippings/2026-04-03-文章标题.md
✓ Images: ./Clippings/attachments/2026-04-03-HiTw93-.../ (15 downloaded)
```
## Alternative: Jina API (Text-only)
For simple text-only fetching without authentication:
```bash
# Single tweet
curl "https://r.jina.ai/https://x.com/USER/status/TWEET_ID" \
-H "Authorization: Bearer ${JINA_API_KEY}"
# Batch fetching
scripts/fetch_tweets.sh url1 url2 url3
```
## Features
### Full Article Mode (fetch_article.py)
- ✅ Structured metadata (author, date, engagement metrics)
- ✅ Automatic image download (all embedded media)
- ✅ Complete Markdown with local image references
- ✅ YAML frontmatter for PKM systems
- ✅ Handles X Articles (long-form content)
### Simple Mode (Jina API)
- Text-only content
- No authentication required beyond Jina API key
- Good for quick text extraction
## Prerequisites ## Prerequisites
You need a Jina API key to use this skill: ### For Full Article Mode
- `uv` (Python package manager)
1. Visit https://jina.ai/ to sign up (free tier available) - No additional setup (twitter-cli auto-installed)
2. Get your API key from the dashboard
3. Set the environment variable:
### For Simple Mode (Jina)
```bash ```bash
export JINA_API_KEY="your_api_key_here" export JINA_API_KEY="your_api_key_here"
# Get from https://jina.ai/
``` ```
## Quick Start ## Output Structure
For a single tweet, use curl directly:
```bash
curl "https://r.jina.ai/https://x.com/USER/status/TWEET_ID" \
-H "Authorization: Bearer ${JINA_API_KEY}"
``` ```
output_dir/
For multiple tweets, use the bundled script: ├── YYYY-MM-DD-article-title.md # Main Markdown file
└── attachments/
```bash └── YYYY-MM-DD-author-title/
scripts/fetch_tweets.sh url1 url2 url3 ├── 01-image.jpg
├── 02-image.jpg
└── ...
``` ```
## What Gets Returned ## What Gets Returned
### Full Article Mode
- **YAML Frontmatter**: source, author, date, likes, retweets, bookmarks
- **Markdown Content**: Full article text with local image references
- **Attachments**: All downloaded images in dedicated folder
### Simple Mode
- **Title**: Post author and content preview - **Title**: Post author and content preview
- **URL Source**: Original tweet link - **URL Source**: Original tweet link
- **Published Time**: GMT timestamp - **Published Time**: GMT timestamp
- **Markdown Content**: Full post text with media descriptions - **Markdown Content**: Text with remote media URLs
## Bundled Scripts
### fetch_tweet.py
Python script for fetching individual tweets.
```bash
python scripts/fetch_tweet.py https://x.com/user/status/123 output.md
```
### fetch_tweets.sh
Bash script for batch fetching multiple tweets.
```bash
scripts/fetch_tweets.sh \
"https://x.com/user/status/123" \
"https://x.com/user/status/456"
```
## URL Formats Supported ## URL Formats Supported
- `https://x.com/USER/status/ID` - `https://x.com/USER/status/ID` (posts)
- `https://twitter.com/USER/status/ID` - `https://x.com/USER/article/ID` (long-form articles)
- `https://x.com/...` (redirects work automatically) - `https://twitter.com/USER/status/ID` (legacy)
## Environment Variables ## Scripts
- `JINA_API_KEY`: Required. Your Jina.ai API key for accessing the reader API ### fetch_article.py
Full-featured article fetcher with image download:
```bash
uv run --with pyyaml python scripts/fetch_article.py <url> [output_dir]
```
### fetch_tweet.py
Simple text-only fetcher using Jina API:
```bash
python scripts/fetch_tweet.py <tweet_url> [output_file]
```
### fetch_tweets.sh
Batch fetch multiple tweets (Jina API):
```bash
scripts/fetch_tweets.sh <url1> <url2> ...
```
## Migration from Jina API
Old workflow:
```bash
curl "https://r.jina.ai/https://x.com/..."
# Manual image extraction and download
```
New workflow:
```bash
uv run --with pyyaml python scripts/fetch_article.py <url>
# Automatic image download, complete Markdown
```

View File

@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""
Fetch Twitter/X Article with images using twitter-cli.
Usage:
python fetch_article.py <article_url> [output_dir]
Example:
python fetch_article.py https://x.com/HiTw93/status/2040047268221608281 ./Clippings
Features:
- Fetches structured data via twitter-cli
- Downloads all images to attachments folder
- Generates Markdown with embedded image references
"""
import sys
import os
import re
import subprocess
import argparse
from pathlib import Path
from datetime import datetime
def run_twitter_cli(url: str) -> dict:
"""Fetch article data using twitter-cli via uv run."""
cmd = ["uv", "run", "--with", "twitter-cli", "twitter", "article", url]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error fetching article: {result.stderr}", file=sys.stderr)
sys.exit(1)
return parse_yaml_output(result.stdout)
def run_jina_api(url: str) -> str:
"""Fetch article text with images using Jina API."""
api_key = os.getenv("JINA_API_KEY", "")
jina_url = f"https://r.jina.ai/{url}"
cmd = ["curl", "-s", jina_url]
if api_key:
cmd.extend(["-H", f"Authorization: Bearer {api_key}"])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Warning: Jina API failed: {result.stderr}", file=sys.stderr)
return ""
return result.stdout
def parse_yaml_output(output: str) -> dict:
"""Parse twitter-cli YAML output into dict."""
try:
import yaml
data = yaml.safe_load(output)
if data.get("ok") and "data" in data:
return data["data"]
return data
except ImportError:
print("Error: PyYAML required. Install with: uv pip install pyyaml", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error parsing YAML: {e}", file=sys.stderr)
sys.exit(1)
def extract_image_urls(text: str) -> list:
"""Extract image URLs from markdown text."""
# Extract all pbs.twimg.com URLs (note: twimg not twitter)
pattern = r'https://pbs\.twimg\.com/media/[^\s\)"\']+'
matches = re.findall(pattern, text)
# Deduplicate and normalize to large size
seen = set()
urls = []
for url in matches:
base_url = url.split('?')[0]
if base_url not in seen:
seen.add(base_url)
urls.append(f"{base_url}?format=jpg&name=large")
return urls
def download_images(image_urls: list, attachments_dir: Path) -> list:
"""Download images and return list of local paths."""
attachments_dir.mkdir(parents=True, exist_ok=True)
local_paths = []
for i, url in enumerate(image_urls, 1):
filename = f"{i:02d}-image.jpg"
filepath = attachments_dir / filename
cmd = ["curl", "-sL", url, "-o", str(filepath)]
result = subprocess.run(cmd, capture_output=True)
if result.returncode == 0 and filepath.exists() and filepath.stat().st_size > 0:
local_paths.append(f"attachments/{attachments_dir.name}/{filename}")
print(f"{filename}")
else:
print(f" ✗ Failed: {filename}")
return local_paths
def replace_image_urls(text: str, image_urls: list, local_paths: list) -> str:
"""Replace remote image URLs with local paths in markdown text."""
for remote_url, local_path in zip(image_urls, local_paths):
# Extract base URL pattern
base_url = remote_url.split('?')[0].replace('?format=jpg&name=large', '')
# Replace all variations of this URL
pattern = re.escape(base_url) + r'(\?[^\)]*)?'
text = re.sub(pattern, local_path, text)
return text
def sanitize_filename(name: str) -> str:
"""Sanitize string for use in filename."""
# Remove special chars, keep alphanumeric, CJK, and some safe chars
name = re.sub(r'[^\w\s\-\u4e00-\u9fff]', '', name)
name = re.sub(r'\s+', '-', name.strip())
return name[:60] # Limit length
def generate_markdown(data: dict, text: str, image_urls: list, local_paths: list, source_url: str) -> str:
"""Generate complete Markdown document."""
# Parse date
created = data.get("createdAtLocal", "")
if created:
date_str = created[:10]
else:
date_str = datetime.now().strftime("%Y-%m-%d")
author = data.get("author", {})
metrics = data.get("metrics", {})
title = data.get("articleTitle", "Untitled")
# Build frontmatter
md = f"""---
source: {source_url}
author: {author.get("name", "")}
date: {date_str}
likes: {metrics.get("likes", 0)}
retweets: {metrics.get("retweets", 0)}
bookmarks: {metrics.get("bookmarks", 0)}
---
# {title}
"""
# Replace image URLs with local paths
if image_urls and local_paths:
text = replace_image_urls(text, image_urls, local_paths)
md += text
return md
def main():
parser = argparse.ArgumentParser(description="Fetch Twitter/X Article with images")
parser.add_argument("url", help="Twitter/X article URL")
parser.add_argument("output_dir", nargs="?", default=".", help="Output directory (default: current)")
args = parser.parse_args()
if not args.url.startswith(("https://x.com/", "https://twitter.com/")):
print("Error: URL must be from x.com or twitter.com", file=sys.stderr)
sys.exit(1)
print(f"Fetching: {args.url}")
print("-" * 50)
# Fetch metadata from twitter-cli
print("Getting metadata...")
data = run_twitter_cli(args.url)
title = data.get("articleTitle", "")
if not title:
print("Error: Could not fetch article data", file=sys.stderr)
sys.exit(1)
author = data.get("author", {})
print(f"Title: {title}")
print(f"Author: {author.get('name', 'Unknown')}")
print(f"Likes: {data.get('metrics', {}).get('likes', 0)}")
# Fetch content with images from Jina API
print("\nGetting content and images...")
jina_content = run_jina_api(args.url)
# Use Jina content if available, otherwise fall back to twitter-cli text
if jina_content:
text = jina_content
# Remove Jina header lines to get clean markdown
# Find "Markdown Content:" and keep everything after it
marker = "Markdown Content:"
idx = text.find(marker)
if idx != -1:
text = text[idx + len(marker):].lstrip()
else:
text = data.get("articleText", "")
# Extract image URLs
image_urls = extract_image_urls(text)
print(f"Images: {len(image_urls)}")
# Setup output paths
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Create attachments folder
date_str = data.get("createdAtLocal", "")[:10] if data.get("createdAtLocal") else datetime.now().strftime("%Y-%m-%d")
safe_author = sanitize_filename(author.get("screenName", "unknown"))
safe_title = sanitize_filename(title)
attachments_name = f"{date_str}-{safe_author}-{safe_title[:30]}"
attachments_dir = output_dir / "attachments" / attachments_name
# Download images
local_paths = []
if image_urls:
print(f"\nDownloading {len(image_urls)} images...")
local_paths = download_images(image_urls, attachments_dir)
# Generate Markdown
md_content = generate_markdown(data, text, image_urls, local_paths, args.url)
# Save Markdown
md_filename = f"{date_str}-{safe_title}.md"
md_path = output_dir / md_filename
md_path.write_text(md_content, encoding="utf-8")
print(f"\n✓ Saved: {md_path}")
if local_paths:
print(f"✓ Images: {attachments_dir} ({len(local_paths)} downloaded)")
return md_path
if __name__ == "__main__":
main()