From 89ad458499095af2e5290aaf4947e53b3e932f74 Mon Sep 17 00:00:00 2001 From: Yangliang Li Date: Thu, 25 Jun 2026 18:09:23 +0800 Subject: [PATCH 1/2] feat(cli): add youtube and instagram commands --- internal/command/root.go | 2 + internal/command/root_sources.go | 36 ++++++++ internal/command/root_sources_test.go | 111 +++++++++++++++++++++++++ internal/command/schema_test_helper.go | 16 ++++ skills/apimux-instagram/SKILL.md | 69 +++++++++++++++ skills/apimux-youtube/SKILL.md | 64 ++++++++++++++ 6 files changed, 298 insertions(+) create mode 100644 skills/apimux-instagram/SKILL.md create mode 100644 skills/apimux-youtube/SKILL.md diff --git a/internal/command/root.go b/internal/command/root.go index 3969a2f..fbab536 100644 --- a/internal/command/root.go +++ b/internal/command/root.go @@ -170,11 +170,13 @@ func (r *Root) newCommand(runCtx *runContext) *cobra.Command { r.newDouyinCommand(runCtx), r.newGoogleAdsCommand(runCtx), r.newGoogleTrendsCommand(runCtx), + r.newInstagramCommand(runCtx), r.newMetaAdsCommand(runCtx), r.newRedditCommand(runCtx), r.newTiktokCommand(runCtx), r.newTrendCloudCommand(runCtx), r.newXiaohongshuCommand(runCtx), + r.newYouTubeCommand(runCtx), ) return rootCmd diff --git a/internal/command/root_sources.go b/internal/command/root_sources.go index 06a7528..6e328f5 100644 --- a/internal/command/root_sources.go +++ b/internal/command/root_sources.go @@ -151,6 +151,25 @@ func (r *Root) newMetaAdsCommand(runCtx *runContext) *cobra.Command { ) } +func (r *Root) newInstagramCommand(runCtx *runContext) *cobra.Command { + return newStaticSourceCommand( + "instagram", + "Instagram profile, media, search, and comments", + "Instagram profile, media, search, and comments.\n\nUse this command to search Instagram users/reels/hashtags, inspect public profiles and posts/reels, and collect comments and replies.", + " apimux instagram search_users --query pimax\n apimux instagram get_user_posts --username pimaxofficial\n apimux instagram get_post_comments --url https://www.instagram.com/p/CODE/", + "search_users, search_reels, search_hashtags, get_user_profile, get_user_posts, get_user_reels, get_post_detail, get_post_comments, get_comment_replies", + newSchemaBoundCapabilityCommand(runCtx, "instagram.search_users", "search_users", "Search Instagram users", "instagram search_users"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.search_reels", "search_reels", "Search Instagram reels", "instagram search_reels"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.search_hashtags", "search_hashtags", "Search Instagram hashtags", "instagram search_hashtags"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.get_user_profile", "get_user_profile", "Fetch one Instagram public profile", "instagram get_user_profile"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.get_user_posts", "get_user_posts", "List Instagram user posts", "instagram get_user_posts"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.get_user_reels", "get_user_reels", "List Instagram user reels", "instagram get_user_reels"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.get_post_detail", "get_post_detail", "Fetch one Instagram post or reel detail", "instagram get_post_detail"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.get_post_comments", "get_post_comments", "List Instagram post comments", "instagram get_post_comments"), + newSchemaBoundCapabilityCommand(runCtx, "instagram.get_comment_replies", "get_comment_replies", "List Instagram comment replies", "instagram get_comment_replies"), + ) +} + func (r *Root) newRedditCommand(runCtx *runContext) *cobra.Command { return newStaticSourceCommand( "reddit", @@ -182,6 +201,23 @@ func (r *Root) newTiktokCommand(runCtx *runContext) *cobra.Command { ) } +func (r *Root) newYouTubeCommand(runCtx *runContext) *cobra.Command { + return newStaticSourceCommand( + "youtube", + "YouTube video, channel, comment, and transcript data", + "YouTube video, channel, comment, and transcript data.\n\nUse this command to search YouTube videos, inspect videos/channels, list comments and replies, and fetch captions or transcript text.", + " apimux youtube search_videos --query 'Pimax VR'\n apimux youtube get_video_comments --url https://www.youtube.com/watch?v=VIDEO_ID\n apimux youtube get_channel_videos --handle @PimaxOfficial", + "search_videos, get_video_detail, get_video_comments, get_comment_replies, get_channel_detail, get_channel_videos, get_video_transcript", + newSchemaBoundCapabilityCommand(runCtx, "youtube.search_videos", "search_videos", "Search YouTube videos", "youtube search_videos"), + newSchemaBoundCapabilityCommand(runCtx, "youtube.get_video_detail", "get_video_detail", "Fetch one YouTube video detail", "youtube get_video_detail"), + newSchemaBoundCapabilityCommand(runCtx, "youtube.get_video_comments", "get_video_comments", "List YouTube video comments", "youtube get_video_comments"), + newSchemaBoundCapabilityCommand(runCtx, "youtube.get_comment_replies", "get_comment_replies", "List YouTube comment replies", "youtube get_comment_replies"), + newSchemaBoundCapabilityCommand(runCtx, "youtube.get_channel_detail", "get_channel_detail", "Fetch one YouTube channel detail", "youtube get_channel_detail"), + newSchemaBoundCapabilityCommand(runCtx, "youtube.get_channel_videos", "get_channel_videos", "List YouTube channel videos or Shorts", "youtube get_channel_videos"), + newSchemaBoundCapabilityCommand(runCtx, "youtube.get_video_transcript", "get_video_transcript", "Fetch YouTube captions or transcript text", "youtube get_video_transcript"), + ) +} + func (r *Root) newXiaohongshuCommand(runCtx *runContext) *cobra.Command { return newStaticSourceCommand( "xiaohongshu", diff --git a/internal/command/root_sources_test.go b/internal/command/root_sources_test.go index 082eefd..d1a9e7d 100644 --- a/internal/command/root_sources_test.go +++ b/internal/command/root_sources_test.go @@ -1219,3 +1219,114 @@ func TestXiaohongshuGetNoteDetailCallsService(t *testing.T) { } assertDataOnlyOutputContains(t, stdout.String(), `"note_id":"67c1f4f1000000001a01b6d3"`) } + +func TestYouTubeSearchVideosCallsService(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if maybeServeSchema(w, r) { + return + } + if r.URL.Path != "/v1/capabilities/youtube.search_videos" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var req struct { + Params map[string]any `json:"params"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode request body: %v", err) + } + if req.Params["query"] != "Pimax VR" || req.Params["publish_time"] != "month" || req.Params["count"] != float64(10) { + t.Fatalf("unexpected params: %#v", req.Params) + } + _, _ = w.Write([]byte(`{"ok":true,"data":[],"meta":{"capability":"youtube.search_videos","cursor":"next"}}`)) + })) + defer server.Close() + + var stdout bytes.Buffer + var stderr bytes.Buffer + + root := NewRoot(&stdout, &stderr) + exitCode, err := root.Execute(context.Background(), []string{ + "--base-url", server.URL, + "youtube", "search_videos", + "--query", "Pimax VR", + "--publish-time", "month", + "--count", "10", + }) + if err != nil { + t.Fatalf("execute root: %v", err) + } + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String()) + } + assertDataOnlyOutputContains(t, stdout.String(), `"data":[]`) +} + +func TestInstagramPostCommentsCallsService(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if maybeServeSchema(w, r) { + return + } + if r.URL.Path != "/v1/capabilities/instagram.get_post_comments" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + var req struct { + Params map[string]any `json:"params"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("decode request body: %v", err) + } + if req.Params["url"] != "https://www.instagram.com/p/CODE/" || req.Params["count"] != float64(20) { + t.Fatalf("unexpected params: %#v", req.Params) + } + _, _ = w.Write([]byte(`{"ok":true,"data":[{"comment_id":"c1","text":"nice"}],"meta":{"capability":"instagram.get_post_comments"}}`)) + })) + defer server.Close() + + var stdout bytes.Buffer + var stderr bytes.Buffer + + root := NewRoot(&stdout, &stderr) + exitCode, err := root.Execute(context.Background(), []string{ + "--base-url", server.URL, + "instagram", "get_post_comments", + "--url", "https://www.instagram.com/p/CODE/", + "--count", "20", + }) + if err != nil { + t.Fatalf("execute root: %v", err) + } + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d, stderr=%s", exitCode, stderr.String()) + } + assertDataOnlyOutputContains(t, stdout.String(), `"comment_id":"c1"`) +} + +func TestInstagramSearchUsersHelpPrintsSchemaFlags(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if maybeServeSchema(w, r) { + return + } + t.Fatalf("unexpected request path: %s", r.URL.Path) + })) + defer server.Close() + + var stdout bytes.Buffer + var stderr bytes.Buffer + + root := NewRoot(&stdout, &stderr) + exitCode, err := root.Execute(context.Background(), []string{ + "--base-url", server.URL, + "instagram", "search_users", + "--help", + }) + if err != nil { + t.Fatalf("execute root: %v", err) + } + if exitCode != 0 { + t.Fatalf("expected exit code 0, got %d, stderr=%s, stdout=%s", exitCode, stderr.String(), stdout.String()) + } + output := stdout.String() + if !strings.Contains(output, "Search Instagram users") || !strings.Contains(output, "--query string") || !strings.Contains(output, "--count int") { + t.Fatalf("expected instagram search_users help with schema flags, got %s", output) + } +} diff --git a/internal/command/schema_test_helper.go b/internal/command/schema_test_helper.go index c38faac..f967a31 100644 --- a/internal/command/schema_test_helper.go +++ b/internal/command/schema_test_helper.go @@ -92,6 +92,22 @@ func testSchemas() map[string]schema.CapabilitySchema { "tiktok.shop_product_info": {Name: "tiktok.shop_product_info", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string"}}}, "tiktok.search_products": {Name: "tiktok.search_products", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "cursor", Type: "string"}, {Name: "offset", Type: "integer"}, {Name: "sort_type", Type: "integer", FlagName: "sort-type"}, {Name: "count", Type: "integer"}}}, "tiktok.product_reviews": {Name: "tiktok.product_reviews", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "page", Type: "integer"}, {Name: "sort", Type: "string", Enum: []string{"default", "latest"}}, {Name: "media_filter", Type: "string", Enum: []string{"all", "media", "verified"}, FlagName: "media-filter"}, {Name: "star", Type: "string", Enum: []string{"all", "1", "2", "3", "4", "5"}}, {Name: "count", Type: "integer"}}}, + "youtube.search_videos": {Name: "youtube.search_videos", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "sort", Type: "string", Enum: []string{"relevance", "upload_date", "view_count", "rating"}}, {Name: "publish_time", Type: "string", Enum: []string{"any", "hour", "today", "week", "month", "year"}, FlagName: "publish-time"}, {Name: "duration", Type: "string", Enum: []string{"any", "short", "medium", "long"}}, {Name: "region", Type: "string"}, {Name: "language", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "youtube.get_video_detail": {Name: "youtube.get_video_detail", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}}}, + "youtube.get_video_comments": {Name: "youtube.get_video_comments", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "sort", Type: "string", Enum: []string{"top", "newest"}}, {Name: "count", Type: "integer"}}}, + "youtube.get_comment_replies": {Name: "youtube.get_comment_replies", Parameters: []schema.CapabilityParam{{Name: "comment_id", Type: "string", Required: true, FlagName: "comment-id"}, {Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "youtube.get_channel_detail": {Name: "youtube.get_channel_detail", Parameters: []schema.CapabilityParam{{Name: "channel_id", Type: "string", FlagName: "channel-id"}, {Name: "handle", Type: "string"}, {Name: "url", Type: "string"}}}, + "youtube.get_channel_videos": {Name: "youtube.get_channel_videos", Parameters: []schema.CapabilityParam{{Name: "channel_id", Type: "string", FlagName: "channel-id"}, {Name: "handle", Type: "string"}, {Name: "url", Type: "string"}, {Name: "content_type", Type: "string", Enum: []string{"video", "shorts", "all"}, FlagName: "content-type"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "youtube.get_video_transcript": {Name: "youtube.get_video_transcript", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", Required: true, FlagName: "video-id"}, {Name: "language", Type: "string"}, {Name: "format", Type: "string", Enum: []string{"segments", "plain"}}}}, + "instagram.search_users": {Name: "instagram.search_users", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "instagram.search_reels": {Name: "instagram.search_reels", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "instagram.search_hashtags": {Name: "instagram.search_hashtags", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "instagram.get_user_profile": {Name: "instagram.get_user_profile", Parameters: []schema.CapabilityParam{{Name: "username", Type: "string"}, {Name: "user_id", Type: "string", FlagName: "user-id"}}}, + "instagram.get_user_posts": {Name: "instagram.get_user_posts", Parameters: []schema.CapabilityParam{{Name: "username", Type: "string"}, {Name: "user_id", Type: "string", FlagName: "user-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "instagram.get_user_reels": {Name: "instagram.get_user_reels", Parameters: []schema.CapabilityParam{{Name: "username", Type: "string"}, {Name: "user_id", Type: "string", FlagName: "user-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "instagram.get_post_detail": {Name: "instagram.get_post_detail", Parameters: []schema.CapabilityParam{{Name: "shortcode", Type: "string"}, {Name: "media_id", Type: "string", FlagName: "media-id"}, {Name: "url", Type: "string"}}}, + "instagram.get_post_comments": {Name: "instagram.get_post_comments", Parameters: []schema.CapabilityParam{{Name: "shortcode", Type: "string"}, {Name: "media_id", Type: "string", FlagName: "media-id"}, {Name: "url", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "instagram.get_comment_replies": {Name: "instagram.get_comment_replies", Parameters: []schema.CapabilityParam{{Name: "comment_id", Type: "string", Required: true, FlagName: "comment-id"}, {Name: "media_id", Type: "string", FlagName: "media-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, "meta_ads.search_ads": {Name: "meta_ads.search_ads", Parameters: []schema.CapabilityParam{{Name: "q", Type: "string", Required: true}, {Name: "country", Type: "string"}, {Name: "ad_type", Type: "string", Enum: []string{"all", "political_and_issue_ads", "housing_ads", "employment_ads", "credit_ads"}, FlagName: "ad-type"}, {Name: "active_status", Type: "string", Enum: []string{"active", "inactive", "all"}, FlagName: "active-status"}, {Name: "media_type", Type: "string", Enum: []string{"all", "video", "image", "meme", "image_and_meme", "none"}, FlagName: "media-type"}, {Name: "platforms", Type: "string"}, {Name: "start_date", Type: "string", FlagName: "start-date"}, {Name: "end_date", Type: "string", FlagName: "end-date"}, {Name: "next_page_token", Type: "string", FlagName: "next-page-token"}}}, "meta_ads.get_ad_detail": {Name: "meta_ads.get_ad_detail", Parameters: []schema.CapabilityParam{{Name: "ad_id", Type: "string", Required: true, FlagName: "ad-id"}}}, "douyin.search_videos": {Name: "douyin.search_videos", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "sort_type", Type: "string", Enum: []string{"comprehensive", "likes", "latest"}, FlagName: "sort-type"}, {Name: "publish_time", Type: "string", Enum: []string{"all", "1d", "1w", "6m"}, FlagName: "publish-time"}, {Name: "filter_duration", Type: "string", Enum: []string{"all", "under_1m", "1m_5m", "over_5m"}, FlagName: "filter-duration"}, {Name: "content_type", Type: "string", Enum: []string{"all", "video", "image", "article"}, FlagName: "content-type"}, {Name: "cursor", Type: "integer"}}}, diff --git a/skills/apimux-instagram/SKILL.md b/skills/apimux-instagram/SKILL.md new file mode 100644 index 0000000..d5aa24b --- /dev/null +++ b/skills/apimux-instagram/SKILL.md @@ -0,0 +1,69 @@ +--- +name: apimux-instagram +version: 1.0.0 +description: "Instagram public profile and media data. Search users, reels, hashtags, inspect profiles/posts/reels, and collect comments and replies for brand monitoring and social research." +metadata: + source: instagram + requires: + bins: ["apimux"] + cliHelp: "apimux instagram --help" +--- + +# Instagram + +Search and inspect public Instagram users, reels, hashtags, posts/reels, comments, and replies. Use this for brand monitoring, creator discovery, UGC research, and public social feedback analysis. + +**Before using:** Read [`../apimux-shared/SKILL.md`](../apimux-shared/SKILL.md) for response structure, error handling, pagination metadata, and CLI conventions. + +## What you can do + +- **Find accounts by brand/name** -> `search_users` +- **Find reels by topic** -> `search_reels` +- **Find hashtags by topic** -> `search_hashtags` +- **Inspect one public profile** -> `get_user_profile` +- **List a user's posts** -> `get_user_posts` +- **List a user's reels** -> `get_user_reels` +- **Inspect one post or reel** -> `get_post_detail` +- **Collect comments** -> `get_post_comments` +- **Collect replies under one comment** -> `get_comment_replies` + +## Common workflows + +- Brand monitoring: `search_users --query pimax` -> `get_user_profile --username ...` -> `get_user_posts`. +- Reel research: `search_reels --query "VR headset"` -> `get_post_detail` -> `get_post_comments`. +- Hashtag scouting: `search_hashtags --query pimax` to discover public topic clusters. +- Comment mining: use `get_post_comments` and `get_comment_replies` to collect customer language and objections. + +## CLI examples + +```bash +apimux instagram search_users --query "pimax" +apimux instagram search_reels --query "VR headset" --count 20 +apimux instagram search_hashtags --query "pimax" +apimux instagram get_user_profile --username "pimaxofficial" +apimux instagram get_user_posts --username "pimaxofficial" --count 30 +apimux instagram get_user_reels --username "pimaxofficial" +apimux instagram get_post_detail --url "https://www.instagram.com/p/CODE/" +apimux instagram get_post_comments --url "https://www.instagram.com/p/CODE/" --count 50 +apimux instagram get_comment_replies --comment-id "COMMENT_ID" --media-id "MEDIA_ID" +``` + +## Capability notes + +| Capability | Required identity | Notes | +|------------|-------------------|-------| +| `search_users` | `query` | Account discovery by keyword. | +| `search_reels` | `query` | Reel discovery by keyword. | +| `search_hashtags` | `query` | Hashtag discovery by keyword. | +| `get_user_profile` | `username` or `user_id` | Provide exactly one user identity. | +| `get_user_posts` | `username` or `user_id` | Paginates through public posts. | +| `get_user_reels` | `username` or `user_id` | Paginates through public reels. | +| `get_post_detail` | `shortcode`, `media_id`, or `url` | Provide exactly one media identity. | +| `get_post_comments` | `shortcode`, `media_id`, or `url` | Returns canonical comments plus pagination metadata when available. | +| `get_comment_replies` | `comment_id` | Pass `media_id` when available to improve provider resolution. | + +## Errors + +- `missing_user_identity`, `ambiguous_user_identity`, `invalid_username`, `invalid_user_id` +- `missing_media_identity`, `ambiguous_media_identity`, `invalid_shortcode`, `invalid_media_id`, `invalid_url` +- `instagram_failed`, `invalid_provider_payload` diff --git a/skills/apimux-youtube/SKILL.md b/skills/apimux-youtube/SKILL.md new file mode 100644 index 0000000..c5620cc --- /dev/null +++ b/skills/apimux-youtube/SKILL.md @@ -0,0 +1,64 @@ +--- +name: apimux-youtube +version: 1.0.0 +description: "YouTube public video and channel data. Search videos, inspect video/channel metadata, collect comments and replies, and fetch transcripts for brand monitoring and social research." +metadata: + source: youtube + requires: + bins: ["apimux"] + cliHelp: "apimux youtube --help" +--- + +# YouTube + +Search and inspect public YouTube videos, channels, comments, replies, and transcripts. Use this for brand monitoring, creator research, competitor tracking, and video/comment summarization. + +**Before using:** Read [`../apimux-shared/SKILL.md`](../apimux-shared/SKILL.md) for response structure, error handling, pagination metadata, and CLI conventions. + +## What you can do + +- **Find videos by topic or brand** -> `search_videos` +- **Inspect one video** -> `get_video_detail` +- **Collect video comments** -> `get_video_comments` +- **Collect replies under one comment** -> `get_comment_replies` +- **Inspect a channel** -> `get_channel_detail` +- **List a creator/channel's videos or Shorts** -> `get_channel_videos` +- **Fetch captions/transcript text** -> `get_video_transcript` + +## Common workflows + +- Brand monitoring: `search_videos --query "Pimax VR"` -> `get_video_detail` -> `get_video_comments`. +- Creator research: `get_channel_detail --handle @PimaxOfficial` -> `get_channel_videos --handle @PimaxOfficial`. +- Summarization: `get_video_transcript --video-id ... --format plain` before summarizing long videos. +- Comment mining: use `get_video_comments --sort newest` for fresh feedback, then `get_comment_replies` for discussion threads. + +## CLI examples + +```bash +apimux youtube search_videos --query "Pimax VR" --publish-time month --count 20 +apimux youtube get_video_detail --url "https://www.youtube.com/watch?v=VIDEO_ID" +apimux youtube get_video_comments --video-id "VIDEO_ID" --sort newest --count 50 +apimux youtube get_comment_replies --comment-id "COMMENT_ID" --video-id "VIDEO_ID" +apimux youtube get_channel_detail --handle "@PimaxOfficial" +apimux youtube get_channel_videos --handle "@PimaxOfficial" --content-type all --count 30 +apimux youtube get_video_transcript --video-id "VIDEO_ID" --language en --format plain +``` + +## Capability notes + +| Capability | Required identity | Notes | +|------------|-------------------|-------| +| `search_videos` | `query` | Supports optional sort, publish time, duration, region, language, cursor, and count. | +| `get_video_detail` | `video_id` or `url` | Provide exactly one identity. | +| `get_video_comments` | `video_id` or `url` | Returns canonical comments plus pagination metadata when available. | +| `get_comment_replies` | `comment_id` | Pass `video_id` when available to improve provider resolution. | +| `get_channel_detail` | `channel_id`, `handle`, or `url` | Provide exactly one channel identity. | +| `get_channel_videos` | `channel_id`, `handle`, or `url` | Use `content_type=video`, `shorts`, or `all`. | +| `get_video_transcript` | `video_id` | Use `format=plain` for summarization input, `segments` for timestamps. | + +## Errors + +- `missing_video_identity`, `ambiguous_video_identity`, `invalid_video_id` +- `missing_channel_identity`, `ambiguous_channel_identity`, `invalid_channel_id`, `invalid_handle` +- `invalid_content_type`, `invalid_format` +- `youtube_failed`, `video_not_found`, `invalid_provider_payload` From 94433b898b78de454bbaf25a1c5c20af31e4ff01 Mon Sep 17 00:00:00 2001 From: Yangliang Li Date: Thu, 25 Jun 2026 18:37:05 +0800 Subject: [PATCH 2/2] fix(cli): align youtube command contract --- internal/command/root_sources_test.go | 3 +-- internal/command/schema_test_helper.go | 4 ++-- skills/apimux-youtube/SKILL.md | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/internal/command/root_sources_test.go b/internal/command/root_sources_test.go index d1a9e7d..8ac9bb4 100644 --- a/internal/command/root_sources_test.go +++ b/internal/command/root_sources_test.go @@ -1234,7 +1234,7 @@ func TestYouTubeSearchVideosCallsService(t *testing.T) { if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("decode request body: %v", err) } - if req.Params["query"] != "Pimax VR" || req.Params["publish_time"] != "month" || req.Params["count"] != float64(10) { + if req.Params["query"] != "Pimax VR" || req.Params["count"] != float64(10) { t.Fatalf("unexpected params: %#v", req.Params) } _, _ = w.Write([]byte(`{"ok":true,"data":[],"meta":{"capability":"youtube.search_videos","cursor":"next"}}`)) @@ -1249,7 +1249,6 @@ func TestYouTubeSearchVideosCallsService(t *testing.T) { "--base-url", server.URL, "youtube", "search_videos", "--query", "Pimax VR", - "--publish-time", "month", "--count", "10", }) if err != nil { diff --git a/internal/command/schema_test_helper.go b/internal/command/schema_test_helper.go index f967a31..e795b10 100644 --- a/internal/command/schema_test_helper.go +++ b/internal/command/schema_test_helper.go @@ -92,9 +92,9 @@ func testSchemas() map[string]schema.CapabilitySchema { "tiktok.shop_product_info": {Name: "tiktok.shop_product_info", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string"}}}, "tiktok.search_products": {Name: "tiktok.search_products", Parameters: []schema.CapabilityParam{{Name: "keyword", Type: "string", Required: true}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "cursor", Type: "string"}, {Name: "offset", Type: "integer"}, {Name: "sort_type", Type: "integer", FlagName: "sort-type"}, {Name: "count", Type: "integer"}}}, "tiktok.product_reviews": {Name: "tiktok.product_reviews", Parameters: []schema.CapabilityParam{{Name: "product_id", Type: "string", Required: true, FlagName: "product-id"}, {Name: "region", Type: "string", Enum: []string{"US", "GB", "SG", "MY", "PH", "TH", "VN", "ID"}}, {Name: "page", Type: "integer"}, {Name: "sort", Type: "string", Enum: []string{"default", "latest"}}, {Name: "media_filter", Type: "string", Enum: []string{"all", "media", "verified"}, FlagName: "media-filter"}, {Name: "star", Type: "string", Enum: []string{"all", "1", "2", "3", "4", "5"}}, {Name: "count", Type: "integer"}}}, - "youtube.search_videos": {Name: "youtube.search_videos", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "sort", Type: "string", Enum: []string{"relevance", "upload_date", "view_count", "rating"}}, {Name: "publish_time", Type: "string", Enum: []string{"any", "hour", "today", "week", "month", "year"}, FlagName: "publish-time"}, {Name: "duration", Type: "string", Enum: []string{"any", "short", "medium", "long"}}, {Name: "region", Type: "string"}, {Name: "language", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, + "youtube.search_videos": {Name: "youtube.search_videos", Parameters: []schema.CapabilityParam{{Name: "query", Type: "string", Required: true}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, "youtube.get_video_detail": {Name: "youtube.get_video_detail", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}}}, - "youtube.get_video_comments": {Name: "youtube.get_video_comments", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "sort", Type: "string", Enum: []string{"top", "newest"}}, {Name: "count", Type: "integer"}}}, + "youtube.get_video_comments": {Name: "youtube.get_video_comments", Parameters: []schema.CapabilityParam{{Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "url", Type: "string"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, "youtube.get_comment_replies": {Name: "youtube.get_comment_replies", Parameters: []schema.CapabilityParam{{Name: "comment_id", Type: "string", Required: true, FlagName: "comment-id"}, {Name: "video_id", Type: "string", FlagName: "video-id"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, "youtube.get_channel_detail": {Name: "youtube.get_channel_detail", Parameters: []schema.CapabilityParam{{Name: "channel_id", Type: "string", FlagName: "channel-id"}, {Name: "handle", Type: "string"}, {Name: "url", Type: "string"}}}, "youtube.get_channel_videos": {Name: "youtube.get_channel_videos", Parameters: []schema.CapabilityParam{{Name: "channel_id", Type: "string", FlagName: "channel-id"}, {Name: "handle", Type: "string"}, {Name: "url", Type: "string"}, {Name: "content_type", Type: "string", Enum: []string{"video", "shorts", "all"}, FlagName: "content-type"}, {Name: "cursor", Type: "string"}, {Name: "count", Type: "integer"}}}, diff --git a/skills/apimux-youtube/SKILL.md b/skills/apimux-youtube/SKILL.md index c5620cc..0a4eeda 100644 --- a/skills/apimux-youtube/SKILL.md +++ b/skills/apimux-youtube/SKILL.md @@ -30,14 +30,14 @@ Search and inspect public YouTube videos, channels, comments, replies, and trans - Brand monitoring: `search_videos --query "Pimax VR"` -> `get_video_detail` -> `get_video_comments`. - Creator research: `get_channel_detail --handle @PimaxOfficial` -> `get_channel_videos --handle @PimaxOfficial`. - Summarization: `get_video_transcript --video-id ... --format plain` before summarizing long videos. -- Comment mining: use `get_video_comments --sort newest` for fresh feedback, then `get_comment_replies` for discussion threads. +- Comment mining: use `get_video_comments --count 50` for audience feedback, then `get_comment_replies` for discussion threads. ## CLI examples ```bash -apimux youtube search_videos --query "Pimax VR" --publish-time month --count 20 +apimux youtube search_videos --query "Pimax VR" --count 20 apimux youtube get_video_detail --url "https://www.youtube.com/watch?v=VIDEO_ID" -apimux youtube get_video_comments --video-id "VIDEO_ID" --sort newest --count 50 +apimux youtube get_video_comments --url "https://www.youtube.com/watch?v=VIDEO_ID" --count 50 apimux youtube get_comment_replies --comment-id "COMMENT_ID" --video-id "VIDEO_ID" apimux youtube get_channel_detail --handle "@PimaxOfficial" apimux youtube get_channel_videos --handle "@PimaxOfficial" --content-type all --count 30 @@ -48,7 +48,7 @@ apimux youtube get_video_transcript --video-id "VIDEO_ID" --language en --format | Capability | Required identity | Notes | |------------|-------------------|-------| -| `search_videos` | `query` | Supports optional sort, publish time, duration, region, language, cursor, and count. | +| `search_videos` | `query` | First wave supports `query`, `cursor`, and `count`. | | `get_video_detail` | `video_id` or `url` | Provide exactly one identity. | | `get_video_comments` | `video_id` or `url` | Returns canonical comments plus pagination metadata when available. | | `get_comment_replies` | `comment_id` | Pass `video_id` when available to improve provider resolution. |