♻️ Unify skillgen to produce MCP-tool-oriented skills
This commit is contained in:
1 parent
ca093caa76
commit
8186963135
11 files changed
+821
-362
No files matched your search
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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,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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user