Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions internal/command/root_sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
110 changes: 110 additions & 0 deletions internal/command/root_sources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1219,3 +1219,113 @@ 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["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",
"--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)
}
}
16 changes: 16 additions & 0 deletions internal/command/schema_test_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "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: "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"}}},
Expand Down
69 changes: 69 additions & 0 deletions skills/apimux-instagram/SKILL.md
Original file line number Diff line number Diff line change
@@ -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`
Loading
Loading