♻️ Unify skillgen to produce MCP-tool-oriented skills

This commit is contained in:
OpenWaygate committed 2026-08-27 23:37:23 +08:00
1 parent ca093caa76
commit 8186963135
11 files changed
+821 -362

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
You are an expert YouTube growth strategist and workflow assistant. You are authenticated to operate YouTube channels on behalf of users via tool calls.
Consult the **youtube** skill for the full tool catalog, safety rules, workflows, and SEO guidelines.
## Workflow
For every user request:
1. **Classify**: Is this a read-only query, a content modification, a deletion, or a multistep workflow?
2. **Gather context**: Use read tools to find resource IDs, verify targets, and understand current state.
3. **Execute**: Use write or delete tools with the gathered data.
4. **Report**: State what was done, including resource IDs and titles.
+122
View File
@@ -0,0 +1,122 @@
---
name: youtube
description: "Use whenever the user mentions YouTube, video uploads, channel management, playlists, video SEO, or any YouTube Data API operation. Manages videos, playlists, comments, captions, subscriptions, thumbnails, analytics, and more."
license: MIT
metadata:
author: eat-pray-ai
homepage: "https://github.com/eat-pray-ai/yutu"
---
# YouTube
Manage YouTube resources via MCP tool calls — abuseReport, activity, caption, channel, channelBanner, channelSection, comment, commentThread, i18nLanguage, i18nRegion, liveBroadcast, liveChatBan, liveChatMessage, liveChatModerator, liveStream, member, membershipsLevel, playlist, playlistImage, playlistItem, search, subscription, superChatEvent, thirdPartyLink, thumbnail, video, videoAbuseReportReason, videoCategory, watermark.
## Key Principles
- **Confirm before deleting**: Always list exactly what will be deleted and get explicit user confirmation before calling any destructive tool.
- **Verify targets**: Before modifying or deleting, use a read tool to confirm the resource exists and matches the user's intent.
- **Preserve existing data**: When updating resources, only modify fields the user explicitly asked to change.
- **Report resource IDs**: Always include resource IDs in responses so the user can reference them.
## Tools
### Read (safe, no side effects)
- `activity-list`
- `caption-download`
- `caption-list`
- `channel-list`
- `channelSection-list`
- `comment-list`
- `commentThread-list`
- `i18nLanguage-list`
- `i18nRegion-list`
- `liveBroadcast-list`
- `liveChatMessage-list`
- `liveChatModerator-list`
- `liveStream-list`
- `member-list`
- `membershipsLevel-list`
- `playlist-list`
- `playlistImage-list`
- `playlistItem-list`
- `search-list`
- `subscription-list`
- `superChatEvent-list`
- `thirdPartyLink-list`
- `video-getRating`
- `video-list`
- `videoAbuseReportReason-list`
- `videoCategory-list`
### Write (create or update content)
- `abuseReport-insert`
- `caption-insert`
- `caption-update`
- `channel-update`
- `channelBanner-insert`
- `comment-insert`
- `comment-markAsSpam`
- `comment-setModerationStatus`
- `comment-update`
- `commentThread-insert`
- `liveBroadcast-bind`
- `liveBroadcast-insert`
- `liveBroadcast-insertCuepoint`
- `liveBroadcast-transition`
- `liveBroadcast-update`
- `liveChatBan-insert`
- `liveChatMessage-insert`
- `liveChatMessage-transition`
- `liveChatModerator-insert`
- `liveStream-insert`
- `liveStream-update`
- `playlist-insert`
- `playlist-update`
- `playlistImage-insert`
- `playlistImage-update`
- `playlistItem-insert`
- `playlistItem-update`
- `subscription-insert`
- `thirdPartyLink-insert`
- `thirdPartyLink-update`
- `thumbnail-set`
- `video-insert`
- `video-rate`
- `video-reportAbuse`
- `video-update`
- `watermark-set`
### Destructive (irreversible deletions — confirm with user first)
- `caption-delete`
- `channelSection-delete`
- `comment-delete`
- `liveBroadcast-delete`
- `liveChatBan-delete`
- `liveChatMessage-delete`
- `liveChatModerator-delete`
- `liveStream-delete`
- `playlist-delete`
- `playlistImage-delete`
- `playlistItem-delete`
- `subscription-delete`
- `thirdPartyLink-delete`
- `video-delete`
- `watermark-unset`
## Common Workflows
See [references/workflows.md](references/workflows.md) for step-by-step walkthroughs.
## YouTube Growth Tips
See [references/seo-guide.md](references/seo-guide.md) for the full guide. When creating or updating content, apply these principles:
- **Titles**: Curiosity gaps + power words. Front-load keywords. Under 60 characters.
- **Descriptions**: First 2 lines appear in search. Include keywords, timestamps, CTAs, 3-5 hashtags.
- **Tags**: Mix broad and long-tail keywords. First 2-3 tags carry the most weight.
- **Thumbnails**: High contrast, 3-4 word text, expressive faces, consistent branding.
- **Publishing**: Post when audience is active. Consistent schedule matters.
- **Engagement**: Pin a comment with a question. Reply within the first hour.
@@ -0,0 +1,61 @@
# YouTube Growth & SEO Guide
Best practices for growing a YouTube channel and optimizing content for discoverability.
## Titles
- Use curiosity gaps, numbers, and power words
- Front-load keywords for search visibility
- Keep under 60 characters to avoid truncation
- A/B test ideas: suggest 2-3 title options when updating
## Descriptions
- First 2 lines are critical -- they appear in search results and above the fold
- Include target keywords naturally in the first 2-3 sentences
- Add timestamps for longer videos (improves watch time and SEO)
- Include calls to action: subscribe, like, comment, related links
- End with relevant hashtags (3-5 max)
## Tags
- Mix broad terms (e.g., "cooking") with specific long-tail keywords (e.g., "easy 15 minute pasta recipe")
- Include common misspellings and alternate phrasings
- Research competitor tags for inspiration
- First 2-3 tags carry the most weight -- make them count
## Thumbnails
- High contrast colors that stand out in the feed
- Readable text: 3-4 words maximum, large bold font
- Expressive faces drive higher CTR
- Consistent branding across videos (recognizable style)
- Avoid clutter -- one clear focal point
## Publishing Strategy
- Post when your audience is most active (check analytics)
- Maintain a consistent upload schedule
- Use YouTube's scheduling feature to publish at optimal times
- First 24-48 hours are critical for algorithm pickup
## Engagement
- Pin a comment with a question to drive discussion
- Reply to comments within the first hour of publishing
- Use community posts to tease upcoming content
- End videos with a clear call to action
## SEO
- Research trending topics in the niche before creating content
- Optimize for both search AND suggested/recommended videos
- Use playlists to increase session watch time
- Add captions/subtitles to improve accessibility and search indexing
## Retention
- Hook viewers in the first 10 seconds
- Use pattern interrupts every 30-60 seconds to maintain attention
- Tease upcoming content within the video ("later I'll show you...")
- Optimize end screens and cards for click-through
@@ -0,0 +1,93 @@
# YouTube Workflows
End-to-end workflows for managing YouTube content using the `yutu` CLI.
## Publishing Video Pipeline
Videos go through a staging queue before publishing. The "Unlisted" playlist acts as an inbox.
### 1. Upload (stage to queue)
Upload a video as unlisted. Use the `--description` field to describe the video content for AI context (what happens, tone, subjects) — this will be rewritten for SEO before publishing.
```bash
CHANNEL_ID=$(yutu channel list --for mine --output json | jq -r '.[0].id')
VIDEO_ID=$(yutu video insert --file video.mp4 \
--title 'Draft title' \
--description 'A husky finds an octopus in shallow water. Every time it gets close, the octopus sprays water at it. Funny and playful tone.' \
--categoryId 22 --privacy unlisted --yes \
--output json | jq -r '.id')
yutu playlistItem insert --kind video \
--playlistId UNLISTED_PLAYLIST_ID --channelId "$CHANNEL_ID" \
--kVideoId "$VIDEO_ID" --yes
```
### 2. Review (AI reads description, generates SEO fields)
Read the staged video's description to understand its content, then generate optimized title, description, tags, and thumbnail.
```bash
yutu video list --ids VIDEO_ID --parts id,snippet,status --output json
```
The AI uses the description as a content brief to generate:
- SEO title (curiosity gap + keywords, under 60 chars)
- SEO description (keywords, timestamps, CTAs, hashtags)
- Tags (broad + long-tail mix)
- Thumbnail guidance
### 3. Publish (update fields + make public + move to target playlist)
Overwrite all fields with SEO-optimized versions, change privacy to public, add to a themed playlist, and remove from the "Unlisted" queue.
```bash
CHANNEL_ID=$(yutu channel list --for mine --output json | jq -r '.[0].id')
# Update video metadata and publish
yutu video update --id VIDEO_ID \
--title 'Husky vs Octopus Water Fight!' \
--description 'Watch this hilarious husky get sprayed by an octopus...' \
--tags 'husky,octopus,funny animals,pets' \
--privacy public --yes
# Set thumbnail
yutu thumbnail set --file thumbnail.jpg --videoId VIDEO_ID
# Add to target playlist
yutu playlistItem insert --kind video \
--playlistId TARGET_PLAYLIST_ID --channelId "$CHANNEL_ID" \
--kVideoId VIDEO_ID --yes
# Remove from "Unlisted" queue (find the playlistItem ID first)
ITEM_ID=$(yutu playlistItem list --playlistId UNLISTED_PLAYLIST_ID --maxResults 0 --output json \
| jq -r '.[] | select(.snippet.resourceId.videoId == "VIDEO_ID") | .id')
yutu playlistItem delete --ids "$ITEM_ID" --yes
```
## Find Unlisted/Private Videos
The search API only returns public videos. To find unlisted or private videos, use the uploads playlist which contains ALL uploads regardless of privacy status.
```bash
# 1. Get the uploads playlist ID (replace UC→UU in channel ID)
UPLOADS=$(yutu channel list --for mine --parts id,contentDetails --output json \
| jq -r '.[0].contentDetails.relatedPlaylists.uploads')
# 2. List all videos in the uploads playlist
yutu playlistItem list --playlistId "$UPLOADS" --maxResults 0 --output json > uploads.json
# 3. Extract video IDs and query privacy status in batches of 50
cat uploads.json | jq -r '.[].snippet.resourceId.videoId' | \
xargs -n50 | while read BATCH; do
yutu video list --ids "$(echo $BATCH | tr ' ' ',')" \
--parts id,snippet,status --output json
done | jq '.[] | select(.status.privacyStatus == "unlisted") | {id, title: .snippet.title, published: .snippet.publishedAt}'
```
Key points:
- `search list` only returns **public** videos.
- The uploads playlist ID follows the pattern: channel `UCxxx` → playlist `UUxxx`.
- `video list --ids` accepts at most 50 IDs per call — batch accordingly.
- The `status` part contains `privacyStatus` (public/unlisted/private).
+10 -2
View File
@@ -2,8 +2,16 @@ load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "skillgen_lib",
srcs = ["main.go"],
embedsrcs = ["setup.md"],
srcs = [
"main.go",
"skill.go",
"types.go",
],
embedsrcs = [
"setup.md",
"seo-guide.md",
"workflows.md",
],
importpath = "github.com/eat-pray-ai/yutu/internal/tools/skillgen",
visibility = ["//visibility:private"],
deps = [
+56 -287
View File
@@ -10,11 +10,8 @@ import (
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/eat-pray-ai/yutu/cmd"
"github.com/spf13/cobra"
// Blank-import every resource package so that its init() registers the
// subcommand (and sub-subcommands) on cmd.RootCmd.
@@ -52,312 +49,84 @@ import (
//go:embed setup.md
var setupContent string
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
//go:embed workflows.md
var workflowsContent string
type verbEntry struct {
name string
short string
//go:embed seo-guide.md
var seoGuideContent string
type skillTarget struct {
subdir string
refs map[string]string
compatibility string
includeSetup bool
}
type resourceEntry struct {
name string
kebab string
human string
short string
long string
verbs []verbEntry
var extSkill = skillTarget{
subdir: filepath.Join("skills", "youtube"),
refs: map[string]string{
"setup.md": setupContent,
"workflows.md": workflowsContent,
"seo-guide.md": seoGuideContent,
},
compatibility: "Requires the yutu CLI binary (installable via npm, brew, or winget) and Google Cloud OAuth credentials for YouTube Data API v3.",
includeSetup: true,
}
type categoryGroup struct {
name string
resources []resourceEntry
var agentSkill = skillTarget{
subdir: filepath.Join("skills", "youtube"),
refs: map[string]string{
"workflows.md": workflowsContent,
"seo-guide.md": seoGuideContent,
},
}
// ---------------------------------------------------------------------------
// Category mapping
// ---------------------------------------------------------------------------
var categoryOrder = []string{
"Content", "Organization", "Community", "Channel", "Discovery", "Metadata",
}
var categoryMap = map[string]string{
"video": "Content",
"caption": "Content",
"thumbnail": "Content",
"watermark": "Content",
"playlist": "Organization",
"playlistItem": "Organization",
"playlistImage": "Organization",
"comment": "Community",
"commentThread": "Community",
"subscription": "Community",
"member": "Community",
"membershipsLevel": "Community",
"liveChatBan": "Community",
"liveChatMessage": "Community",
"liveChatModerator": "Community",
"liveBroadcast": "Content",
"liveStream": "Content",
"superChatEvent": "Community",
"channel": "Channel",
"channelBanner": "Channel",
"channelSection": "Channel",
"thirdPartyLink": "Channel",
"search": "Discovery",
"activity": "Discovery",
"videoCategory": "Metadata",
"abuseReport": "Community",
"videoAbuseReportReason": "Metadata",
"i18nLanguage": "Metadata",
"i18nRegion": "Metadata",
}
// resourceCategory returns the category for a resource, or "Other" if unmapped.
func resourceCategory(name string) string {
if cat, ok := categoryMap[name]; ok {
return cat
func generateSkill(baseDir string, t skillTarget, resources []resourceEntry) {
dir := filepath.Join(baseDir, t.subdir)
refDir := filepath.Join(dir, "references")
if err := os.MkdirAll(refDir, 0o755); err != nil {
log.Fatalf("mkdir %s: %v", refDir, err)
}
return "Other"
}
// collectResource builds a resourceEntry from a cobra resource command.
func collectResource(c *cobra.Command) resourceEntry {
name := c.Name()
var verbs []verbEntry
for _, sub := range c.Commands() {
if sub.Name() == "help" {
continue
for name, content := range t.refs {
if err := os.WriteFile(filepath.Join(refDir, name), []byte(content), 0o644); err != nil {
log.Fatalf("write %s: %v", name, err)
}
verbs = append(verbs, verbEntry{name: sub.Name(), short: sub.Short})
}
sort.Slice(
verbs, func(i, j int) bool {
return verbs[i].name < verbs[j].name
},
)
return resourceEntry{
name: name,
kebab: camelToKebab(name),
human: camelToWords(name),
short: c.Short,
long: c.Long,
verbs: verbs,
}
}
// groupByCategory groups resources into categories ordered by categoryOrder,
// with "Other" appended at the end for any unmapped resources.
func groupByCategory(resources []resourceEntry) []categoryGroup {
byCategory := make(map[string][]resourceEntry)
path := filepath.Join(dir, "SKILL.md")
if err := writeSkill(path, resources, t); err != nil {
log.Fatalf("write %s: %v", path, err)
}
totalVerbs := 0
for _, r := range resources {
cat := resourceCategory(r.name)
byCategory[cat] = append(byCategory[cat], r)
totalVerbs += len(r.verbs)
}
var groups []categoryGroup
for _, cat := range categoryOrder {
if rs, ok := byCategory[cat]; ok {
sort.Slice(
rs, func(i, j int) bool {
return rs[i].name < rs[j].name
},
)
groups = append(groups, categoryGroup{name: cat, resources: rs})
delete(byCategory, cat)
}
}
// Append "Other" for any remaining unmapped categories.
if rs, ok := byCategory["Other"]; ok {
sort.Slice(
rs, func(i, j int) bool {
return rs[i].name < rs[j].name
},
)
groups = append(groups, categoryGroup{name: "Other", resources: rs})
}
return groups
fmt.Printf("Generated skill: %d resources, %d verbs → %s\n", len(resources), totalVerbs, path)
}
// ---------------------------------------------------------------------------
// Unified description builder
// ---------------------------------------------------------------------------
// buildUnifiedDescription constructs the skill description with high-frequency
// triggers and a broad fallback. Intentionally concise — no exhaustive verb list.
func buildUnifiedDescription(_ []categoryGroup) string {
return "Use whenever the user mentions YouTube, video uploads, channel management, playlists, video SEO, or any YouTube Data API operation. Manages videos, playlists, comments, captions, subscriptions, thumbnails, analytics, and more via the yutu CLI."
}
// ---------------------------------------------------------------------------
// Static content for SKILL.md sections
// ---------------------------------------------------------------------------
const workflowSummary = `| Task | Quick Command |
|------|---------------|
| Publishing video pipeline | ` + "`yutu video insert --privacy unlisted`" + ` → review → ` + "`yutu video update --privacy public`" + ` |
| Find unlisted/private videos | ` + "`yutu playlistItem list`" + ` (uploads playlist) → ` + "`yutu video list --parts id,snippet,status`" + ` |`
const growthTips = `- **Titles**: Curiosity gaps + power words. Front-load keywords. Under 60 characters.
- **Descriptions**: First 2 lines appear in search. Include keywords, timestamps, CTAs, 3-5 hashtags.
- **Tags**: Mix broad and long-tail keywords. First 2-3 tags carry the most weight.
- **Thumbnails**: High contrast, 3-4 word text, expressive faces, consistent branding.
- **Publishing**: Post when audience is active. Consistent schedule matters.
- **Engagement**: Pin a comment with a question. Reply within the first hour.`
// ---------------------------------------------------------------------------
// Unified SKILL.md writer
// ---------------------------------------------------------------------------
// writeUnifiedSkill generates the single unified SKILL.md for the youtube skill.
func writeUnifiedSkill(path string, groups []categoryGroup) error {
var b strings.Builder
desc := buildUnifiedDescription(groups)
_, _ = fmt.Fprintf(
&b, `---
name: youtube
description: "%s"
license: MIT
compatibility: Requires the yutu CLI binary (installable via npm, brew, or winget) and Google Cloud OAuth credentials for YouTube Data API v3.
metadata:
author: eat-pray-ai
homepage: "https://github.com/eat-pray-ai/yutu"
---
`, strings.ReplaceAll(desc, `"`, `\"`),
)
b.WriteString("# YouTube\n\n")
b.WriteString("Manage YouTube resources using the `yutu` CLI — videos, playlists, comments, channels, captions, subscriptions, and more.\n\n")
b.WriteString("## Quick Start\n\n")
b.WriteString("1. Ensure `yutu` is installed and authenticated. If not, follow [references/setup.md](references/setup.md).\n")
b.WriteString("2. Identify the resource and operation from the tables below.\n")
b.WriteString("3. Run `yutu <resource> <operation> -h` for full flag details on any command.\n")
b.WriteString("4. For multistep tasks (upload + thumbnail + playlist), see [references/workflows.md](references/workflows.md).\n\n")
b.WriteString("## Key Principles\n\n")
b.WriteString("- **Run `yutu <resource> <operation> -h` before executing a command** — flags vary between subcommands (e.g., `playlist list` uses `--mine` boolean flag, while `channel list` uses `--for mine` string flag). Never guess flag syntax.\n")
b.WriteString("- Always verify before destructive operations — deletions are irreversible.\n")
b.WriteString("- Use `--output json` when you need to parse or chain results.\n")
b.WriteString("- Get your channel ID with `yutu channel list --for mine` — many operations need it.\n")
b.WriteString("- When updating metadata, only specify the fields you want to change.\n\n")
b.WriteString("## Operations\n\n")
for _, g := range groups {
_, _ = fmt.Fprintf(&b, "### %s\n\n", g.name)
b.WriteString("| Resource | Operations |\n")
b.WriteString("|----------|------------|\n")
for _, r := range g.resources {
var names []string
for _, v := range r.verbs {
names = append(names, v.name)
}
_, _ = fmt.Fprintf(&b, "| %s | %s |\n", r.name, strings.Join(names, ", "))
}
b.WriteString("\n")
}
b.WriteString("## Common Workflows\n\n")
b.WriteString("See [references/workflows.md](references/workflows.md) for step-by-step walkthroughs of each task below.\n\n")
b.WriteString(workflowSummary + "\n\n")
b.WriteString("## YouTube Growth Tips\n\n")
b.WriteString("See [references/seo-guide.md](references/seo-guide.md) for the full guide. When uploading or updating video metadata, apply these principles:\n\n")
b.WriteString(growthTips + "\n")
return os.WriteFile(path, []byte(b.String()), 0o644)
}
// ---------------------------------------------------------------------------
// Setup writer
// ---------------------------------------------------------------------------
func writeSetup(path string) error {
return os.WriteFile(path, []byte(setupContent), 0o644)
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
func main() {
out := flag.String("out", "./skills", "output directory for generated skills")
skillDir := flag.String("skill-dir", ".", "base directory for generated skill")
instructionDir := flag.String("instruction-dir", "./cmd/agent", "base directory for generated instruction and agent skill")
flag.Parse()
root := cmd.RootCmd
root.InitDefaultHelpCmd()
resources := collectResources(root)
dir := filepath.Join(*out, "youtube")
refDir := filepath.Join(dir, "references")
// External skill: includes setup.md for installation.
generateSkill(*skillDir, extSkill, resources)
if err := os.MkdirAll(refDir, 0o755); err != nil {
log.Fatalf("mkdir %s: %v", refDir, err)
// Agent instruction.
if err := os.MkdirAll(*instructionDir, 0o755); err != nil {
log.Fatalf("mkdir %s: %v", *instructionDir, err)
}
setupPath := filepath.Join(refDir, "setup.md")
if err := writeSetup(setupPath); err != nil {
log.Fatalf("write setup %s: %v", setupPath, err)
instrPath := filepath.Join(*instructionDir, "INSTRUCTION.md")
if err := writeInstruction(instrPath); err != nil {
log.Fatalf("write %s: %v", instrPath, err)
}
fmt.Printf("Generated instruction: → %s\n", instrPath)
var resources []resourceEntry
for _, c := range root.Commands() {
if !strings.HasPrefix(c.Short, "Manage") {
continue
}
resources = append(resources, collectResource(c))
}
groups := groupByCategory(resources)
skillPath := filepath.Join(dir, "SKILL.md")
if err := writeUnifiedSkill(skillPath, groups); err != nil {
log.Fatalf("write skill %s: %v", skillPath, err)
}
totalVerbs := 0
for _, g := range groups {
for _, r := range g.resources {
totalVerbs += len(r.verbs)
}
}
fmt.Printf(
"Generated unified youtube skill: %d resources, %d verbs, %d categories\n",
len(resources), totalVerbs, len(groups),
)
}
// ---------------------------------------------------------------------------
// String utilities
// ---------------------------------------------------------------------------
// camelToWords splits a camelCase string into lowercase space-separated words.
func camelToWords(s string) string {
return strings.Join(camelSplit(s), " ")
}
func camelToKebab(s string) string {
return strings.Join(camelSplit(s), "-")
}
func camelSplit(s string) []string {
var words []string
var cur []byte
for i := range len(s) {
ch := s[i]
if ch >= 'A' && ch <= 'Z' && len(cur) > 0 {
words = append(words, strings.ToLower(string(cur)))
cur = cur[:0]
}
cur = append(cur, ch)
}
if len(cur) > 0 {
words = append(words, strings.ToLower(string(cur)))
}
return words
}
// Agent skill: embedded in the binary, no setup needed.
generateSkill(*instructionDir, agentSkill, resources)
}
+61
View File
@@ -0,0 +1,61 @@
# YouTube Growth & SEO Guide
Best practices for growing a YouTube channel and optimizing content for discoverability.
## Titles
- Use curiosity gaps, numbers, and power words
- Front-load keywords for search visibility
- Keep under 60 characters to avoid truncation
- A/B test ideas: suggest 2-3 title options when updating
## Descriptions
- First 2 lines are critical -- they appear in search results and above the fold
- Include target keywords naturally in the first 2-3 sentences
- Add timestamps for longer videos (improves watch time and SEO)
- Include calls to action: subscribe, like, comment, related links
- End with relevant hashtags (3-5 max)
## Tags
- Mix broad terms (e.g., "cooking") with specific long-tail keywords (e.g., "easy 15 minute pasta recipe")
- Include common misspellings and alternate phrasings
- Research competitor tags for inspiration
- First 2-3 tags carry the most weight -- make them count
## Thumbnails
- High contrast colors that stand out in the feed
- Readable text: 3-4 words maximum, large bold font
- Expressive faces drive higher CTR
- Consistent branding across videos (recognizable style)
- Avoid clutter -- one clear focal point
## Publishing Strategy
- Post when your audience is most active (check analytics)
- Maintain a consistent upload schedule
- Use YouTube's scheduling feature to publish at optimal times
- First 24-48 hours are critical for algorithm pickup
## Engagement
- Pin a comment with a question to drive discussion
- Reply to comments within the first hour of publishing
- Use community posts to tease upcoming content
- End videos with a clear call to action
## SEO
- Research trending topics in the niche before creating content
- Optimize for both search AND suggested/recommended videos
- Use playlists to increase session watch time
- Add captions/subtitles to improve accessibility and search indexing
## Retention
- Hook viewers in the first 10 seconds
- Use pattern interrupts every 30-60 seconds to maintain attention
- Tease upcoming content within the video ("later I'll show you...")
- Optimize end screens and cards for click-through
+140
View File
@@ -0,0 +1,140 @@
// Copyright 2026 eat-pray-ai & OpenWaygate
// SPDX-License-Identifier: Apache-2.0
package main
import (
"fmt"
"os"
"strings"
)
const skillDescription = "Use whenever the user mentions YouTube, video uploads, channel management, playlists, video SEO, or any YouTube Data API operation. Manages videos, playlists, comments, captions, subscriptions, thumbnails, analytics, and more."
const growthTips = `- **Titles**: Curiosity gaps + power words. Front-load keywords. Under 60 characters.
- **Descriptions**: First 2 lines appear in search. Include keywords, timestamps, CTAs, 3-5 hashtags.
- **Tags**: Mix broad and long-tail keywords. First 2-3 tags carry the most weight.
- **Thumbnails**: High contrast, 3-4 word text, expressive faces, consistent branding.
- **Publishing**: Post when audience is active. Consistent schedule matters.
- **Engagement**: Pin a comment with a question. Reply within the first hour.`
const safetyRules = `- **Confirm before deleting**: Always list exactly what will be deleted and get explicit user confirmation before calling any destructive tool.
- **Verify targets**: Before modifying or deleting, use a read tool to confirm the resource exists and matches the user's intent.
- **Preserve existing data**: When updating resources, only modify fields the user explicitly asked to change.
- **Report resource IDs**: Always include resource IDs in responses so the user can reference them.`
const workflow = `For every user request:
1. **Classify**: Is this a read-only query, a content modification, a deletion, or a multistep workflow?
2. **Gather context**: Use read tools to find resource IDs, verify targets, and understand current state.
3. **Execute**: Use write or delete tools with the gathered data.
4. **Report**: State what was done, including resource IDs and titles.`
type toolClass int
const (
classRead toolClass = iota
classWrite
classDelete
)
var destructiveVerbs = map[string]bool{
"delete": true, "unset": true,
}
var readOnlyVerbs = map[string]bool{
"list": true, "download": true, "getRating": true,
}
func classifyVerb(verb string) toolClass {
if destructiveVerbs[verb] {
return classDelete
}
if readOnlyVerbs[verb] {
return classRead
}
return classWrite
}
func writeFrontmatter(b *strings.Builder, t skillTarget) {
_, _ = fmt.Fprintf(
b, "---\nname: youtube\ndescription: \"%s\"\nlicense: MIT\n",
skillDescription,
)
if t.compatibility != "" {
_, _ = fmt.Fprintf(b, "compatibility: %s\n", t.compatibility)
}
b.WriteString("metadata:\n author: eat-pray-ai\n homepage: \"https://github.com/eat-pray-ai/yutu\"\n---\n\n")
}
func writeSkill(path string, resources []resourceEntry, t skillTarget) error {
var b strings.Builder
writeFrontmatter(&b, t)
b.WriteString("# YouTube\n\n")
names := make([]string, len(resources))
for i, r := range resources {
names[i] = r.name
}
_, _ = fmt.Fprintf(&b, "Manage YouTube resources via MCP tool calls — %s.\n\n", strings.Join(names, ", "))
if t.includeSetup {
b.WriteString("## Setup\n\n")
b.WriteString("Ensure `yutu` is installed and running as an MCP server. See [references/setup.md](references/setup.md).\n\n")
}
b.WriteString("## Key Principles\n\n")
b.WriteString(safetyRules + "\n\n")
var readTools, writeTools, deleteTools []string
for _, r := range resources {
for _, v := range r.verbs {
toolName := r.name + "-" + v.name
switch classifyVerb(v.name) {
case classRead:
readTools = append(readTools, toolName)
case classWrite:
writeTools = append(writeTools, toolName)
case classDelete:
deleteTools = append(deleteTools, toolName)
}
}
}
b.WriteString("## Tools\n\n")
b.WriteString("### Read (safe, no side effects)\n\n")
for _, t := range readTools {
_, _ = fmt.Fprintf(&b, "- `%s`\n", t)
}
b.WriteString("\n### Write (create or update content)\n\n")
for _, t := range writeTools {
_, _ = fmt.Fprintf(&b, "- `%s`\n", t)
}
b.WriteString("\n### Destructive (irreversible deletions — confirm with user first)\n\n")
for _, t := range deleteTools {
_, _ = fmt.Fprintf(&b, "- `%s`\n", t)
}
b.WriteString("\n## Common Workflows\n\n")
b.WriteString("See [references/workflows.md](references/workflows.md) for step-by-step walkthroughs.\n\n")
b.WriteString("## YouTube Growth Tips\n\n")
b.WriteString("See [references/seo-guide.md](references/seo-guide.md) for the full guide. When creating or updating content, apply these principles:\n\n")
b.WriteString(growthTips + "\n")
return os.WriteFile(path, []byte(b.String()), 0o644)
}
func writeInstruction(path string) error {
var b strings.Builder
b.WriteString("You are an expert YouTube growth strategist and workflow assistant. You are authenticated to operate YouTube channels on behalf of users via tool calls.\n\n")
b.WriteString("Consult the **youtube** skill for the full tool catalog, safety rules, workflows, and SEO guidelines.\n\n")
b.WriteString("## Workflow\n\n")
b.WriteString(workflow + "\n")
return os.WriteFile(path, []byte(b.String()), 0o644)
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 eat-pray-ai & OpenWaygate
// SPDX-License-Identifier: Apache-2.0
package main
import (
"sort"
"strings"
"github.com/spf13/cobra"
)
type verbEntry struct {
name string
short string
}
type resourceEntry struct {
name string
kebab string
human string
short string
long string
verbs []verbEntry
}
func camelToWords(s string) string {
return strings.Join(camelSplit(s), " ")
}
func camelToKebab(s string) string {
return strings.Join(camelSplit(s), "-")
}
func camelSplit(s string) []string {
var words []string
var cur []byte
for i := range len(s) {
ch := s[i]
if ch >= 'A' && ch <= 'Z' && len(cur) > 0 {
words = append(words, strings.ToLower(string(cur)))
cur = cur[:0]
}
cur = append(cur, ch)
}
if len(cur) > 0 {
words = append(words, strings.ToLower(string(cur)))
}
return words
}
func collectResources(root *cobra.Command) []resourceEntry {
var resources []resourceEntry
for _, c := range root.Commands() {
if !strings.HasPrefix(c.Short, "Manage") {
continue
}
name := c.Name()
var verbs []verbEntry
for _, sub := range c.Commands() {
if sub.Name() == "help" {
continue
}
verbs = append(verbs, verbEntry{name: sub.Name(), short: sub.Short})
}
sort.Slice(verbs, func(i, j int) bool {
return verbs[i].name < verbs[j].name
})
resources = append(resources, resourceEntry{
name: name,
kebab: camelToKebab(name),
human: camelToWords(name),
short: c.Short,
long: c.Long,
verbs: verbs,
})
}
sort.Slice(resources, func(i, j int) bool {
return resources[i].name < resources[j].name
})
return resources
}
+93
View File
@@ -0,0 +1,93 @@
# YouTube Workflows
End-to-end workflows for managing YouTube content using the `yutu` CLI.
## Publishing Video Pipeline
Videos go through a staging queue before publishing. The "Unlisted" playlist acts as an inbox.
### 1. Upload (stage to queue)
Upload a video as unlisted. Use the `--description` field to describe the video content for AI context (what happens, tone, subjects) — this will be rewritten for SEO before publishing.
```bash
CHANNEL_ID=$(yutu channel list --for mine --output json | jq -r '.[0].id')
VIDEO_ID=$(yutu video insert --file video.mp4 \
--title 'Draft title' \
--description 'A husky finds an octopus in shallow water. Every time it gets close, the octopus sprays water at it. Funny and playful tone.' \
--categoryId 22 --privacy unlisted --yes \
--output json | jq -r '.id')
yutu playlistItem insert --kind video \
--playlistId UNLISTED_PLAYLIST_ID --channelId "$CHANNEL_ID" \
--kVideoId "$VIDEO_ID" --yes
```
### 2. Review (AI reads description, generates SEO fields)
Read the staged video's description to understand its content, then generate optimized title, description, tags, and thumbnail.
```bash
yutu video list --ids VIDEO_ID --parts id,snippet,status --output json
```
The AI uses the description as a content brief to generate:
- SEO title (curiosity gap + keywords, under 60 chars)
- SEO description (keywords, timestamps, CTAs, hashtags)
- Tags (broad + long-tail mix)
- Thumbnail guidance
### 3. Publish (update fields + make public + move to target playlist)
Overwrite all fields with SEO-optimized versions, change privacy to public, add to a themed playlist, and remove from the "Unlisted" queue.
```bash
CHANNEL_ID=$(yutu channel list --for mine --output json | jq -r '.[0].id')
# Update video metadata and publish
yutu video update --id VIDEO_ID \
--title 'Husky vs Octopus Water Fight!' \
--description 'Watch this hilarious husky get sprayed by an octopus...' \
--tags 'husky,octopus,funny animals,pets' \
--privacy public --yes
# Set thumbnail
yutu thumbnail set --file thumbnail.jpg --videoId VIDEO_ID
# Add to target playlist
yutu playlistItem insert --kind video \
--playlistId TARGET_PLAYLIST_ID --channelId "$CHANNEL_ID" \
--kVideoId VIDEO_ID --yes
# Remove from "Unlisted" queue (find the playlistItem ID first)
ITEM_ID=$(yutu playlistItem list --playlistId UNLISTED_PLAYLIST_ID --maxResults 0 --output json \
| jq -r '.[] | select(.snippet.resourceId.videoId == "VIDEO_ID") | .id')
yutu playlistItem delete --ids "$ITEM_ID" --yes
```
## Find Unlisted/Private Videos
The search API only returns public videos. To find unlisted or private videos, use the uploads playlist which contains ALL uploads regardless of privacy status.
```bash
# 1. Get the uploads playlist ID (replace UC→UU in channel ID)
UPLOADS=$(yutu channel list --for mine --parts id,contentDetails --output json \
| jq -r '.[0].contentDetails.relatedPlaylists.uploads')
# 2. List all videos in the uploads playlist
yutu playlistItem list --playlistId "$UPLOADS" --maxResults 0 --output json > uploads.json
# 3. Extract video IDs and query privacy status in batches of 50
cat uploads.json | jq -r '.[].snippet.resourceId.videoId' | \
xargs -n50 | while read BATCH; do
yutu video list --ids "$(echo $BATCH | tr ' ' ',')" \
--parts id,snippet,status --output json
done | jq '.[] | select(.status.privacyStatus == "unlisted") | {id, title: .snippet.title, published: .snippet.publishedAt}'
```
Key points:
- `search list` only returns **public** videos.
- The uploads playlist ID follows the pattern: channel `UCxxx` → playlist `UUxxx`.
- `video list --ids` accepts at most 50 IDs per call — batch accordingly.
- The `status` part contains `privacyStatus` (public/unlisted/private).
+91 -73
View File
@@ -1,6 +1,6 @@
---
name: youtube
description: "Use whenever the user mentions YouTube, video uploads, channel management, playlists, video SEO, or any YouTube Data API operation. Manages videos, playlists, comments, captions, subscriptions, thumbnails, analytics, and more via the yutu CLI."
description: "Use whenever the user mentions YouTube, video uploads, channel management, playlists, video SEO, or any YouTube Data API operation. Manages videos, playlists, comments, captions, subscriptions, thumbnails, analytics, and more."
license: MIT
compatibility: Requires the yutu CLI binary (installable via npm, brew, or winget) and Google Cloud OAuth credentials for YouTube Data API v3.
metadata:
@@ -10,96 +10,114 @@ metadata:
# YouTube
Manage YouTube resources using the `yutu` CLI — videos, playlists, comments, channels, captions, subscriptions, and more.
Manage YouTube resources via MCP tool calls — abuseReport, activity, caption, channel, channelBanner, channelSection, comment, commentThread, i18nLanguage, i18nRegion, liveBroadcast, liveChatBan, liveChatMessage, liveChatModerator, liveStream, member, membershipsLevel, playlist, playlistImage, playlistItem, search, subscription, superChatEvent, thirdPartyLink, thumbnail, video, videoAbuseReportReason, videoCategory, watermark.
## Quick Start
## Setup
1. Ensure `yutu` is installed and authenticated. If not, follow [references/setup.md](references/setup.md).
2. Identify the resource and operation from the tables below.
3. Run `yutu <resource> <operation> -h` for full flag details on any command.
4. For multistep tasks (upload + thumbnail + playlist), see [references/workflows.md](references/workflows.md).
Ensure `yutu` is installed and running as an MCP server. See [references/setup.md](references/setup.md).
## Key Principles
- **Run `yutu <resource> <operation> -h` before executing a command** — flags vary between subcommands (e.g., `playlist list` uses `--mine` boolean flag, while `channel list` uses `--for mine` string flag). Never guess flag syntax.
- Always verify before destructive operations — deletions are irreversible.
- Use `--output json` when you need to parse or chain results.
- Get your channel ID with `yutu channel list --for mine` — many operations need it.
- When updating metadata, only specify the fields you want to change.
- **Confirm before deleting**: Always list exactly what will be deleted and get explicit user confirmation before calling any destructive tool.
- **Verify targets**: Before modifying or deleting, use a read tool to confirm the resource exists and matches the user's intent.
- **Preserve existing data**: When updating resources, only modify fields the user explicitly asked to change.
- **Report resource IDs**: Always include resource IDs in responses so the user can reference them.
## Operations
## Tools
### Content
### Read (safe, no side effects)
| Resource | Operations |
|----------|------------|
| caption | delete, download, insert, list, update |
| liveBroadcast | bind, delete, insert, insertCuepoint, list, transition, update |
| liveStream | delete, insert, list, update |
| thumbnail | set |
| video | delete, getRating, insert, list, rate, reportAbuse, update |
| watermark | set, unset |
- `activity-list`
- `caption-download`
- `caption-list`
- `channel-list`
- `channelSection-list`
- `comment-list`
- `commentThread-list`
- `i18nLanguage-list`
- `i18nRegion-list`
- `liveBroadcast-list`
- `liveChatMessage-list`
- `liveChatModerator-list`
- `liveStream-list`
- `member-list`
- `membershipsLevel-list`
- `playlist-list`
- `playlistImage-list`
- `playlistItem-list`
- `search-list`
- `subscription-list`
- `superChatEvent-list`
- `thirdPartyLink-list`
- `video-getRating`
- `video-list`
- `videoAbuseReportReason-list`
- `videoCategory-list`
### Organization
### Write (create or update content)
| Resource | Operations |
|----------|------------|
| playlist | delete, insert, list, update |
| playlistImage | delete, insert, list, update |
| playlistItem | delete, insert, list, update |
- `abuseReport-insert`
- `caption-insert`
- `caption-update`
- `channel-update`
- `channelBanner-insert`
- `comment-insert`
- `comment-markAsSpam`
- `comment-setModerationStatus`
- `comment-update`
- `commentThread-insert`
- `liveBroadcast-bind`
- `liveBroadcast-insert`
- `liveBroadcast-insertCuepoint`
- `liveBroadcast-transition`
- `liveBroadcast-update`
- `liveChatBan-insert`
- `liveChatMessage-insert`
- `liveChatMessage-transition`
- `liveChatModerator-insert`
- `liveStream-insert`
- `liveStream-update`
- `playlist-insert`
- `playlist-update`
- `playlistImage-insert`
- `playlistImage-update`
- `playlistItem-insert`
- `playlistItem-update`
- `subscription-insert`
- `thirdPartyLink-insert`
- `thirdPartyLink-update`
- `thumbnail-set`
- `video-insert`
- `video-rate`
- `video-reportAbuse`
- `video-update`
- `watermark-set`
### Community
### Destructive (irreversible deletions — confirm with user first)
| Resource | Operations |
|----------|------------|
| abuseReport | insert |
| comment | delete, insert, list, markAsSpam, setModerationStatus, update |
| commentThread | insert, list |
| liveChatBan | delete, insert |
| liveChatMessage | delete, insert, list, transition |
| liveChatModerator | delete, insert, list |
| member | list |
| membershipsLevel | list |
| subscription | delete, insert, list |
| superChatEvent | list |
### Channel
| Resource | Operations |
|----------|------------|
| channel | list, update |
| channelBanner | insert |
| channelSection | delete, list |
| thirdPartyLink | delete, insert, list, update |
### Discovery
| Resource | Operations |
|----------|------------|
| activity | list |
| search | list |
### Metadata
| Resource | Operations |
|----------|------------|
| i18nLanguage | list |
| i18nRegion | list |
| videoAbuseReportReason | list |
| videoCategory | list |
- `caption-delete`
- `channelSection-delete`
- `comment-delete`
- `liveBroadcast-delete`
- `liveChatBan-delete`
- `liveChatMessage-delete`
- `liveChatModerator-delete`
- `liveStream-delete`
- `playlist-delete`
- `playlistImage-delete`
- `playlistItem-delete`
- `subscription-delete`
- `thirdPartyLink-delete`
- `video-delete`
- `watermark-unset`
## Common Workflows
See [references/workflows.md](references/workflows.md) for step-by-step walkthroughs of each task below.
| Task | Quick Command |
|------|---------------|
| Publishing video pipeline | `yutu video insert --privacy unlisted` → review → `yutu video update --privacy public` |
| Find unlisted/private videos | `yutu playlistItem list` (uploads playlist) → `yutu video list --parts id,snippet,status` |
See [references/workflows.md](references/workflows.md) for step-by-step walkthroughs.
## YouTube Growth Tips
See [references/seo-guide.md](references/seo-guide.md) for the full guide. When uploading or updating video metadata, apply these principles:
See [references/seo-guide.md](references/seo-guide.md) for the full guide. When creating or updating content, apply these principles:
- **Titles**: Curiosity gaps + power words. Front-load keywords. Under 60 characters.
- **Descriptions**: First 2 lines appear in search. Include keywords, timestamps, CTAs, 3-5 hashtags.