chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
import type { ToolConfig } from '@/tools/types'
import type { YouTubeChannelInfoParams, YouTubeChannelInfoResponse } from '@/tools/youtube/types'
export const youtubeChannelInfoTool: ToolConfig<
YouTubeChannelInfoParams,
YouTubeChannelInfoResponse
> = {
id: 'youtube_channel_info',
name: 'YouTube Channel Info',
description:
'Get detailed information about a YouTube channel including statistics, branding, and content details.',
version: '1.1.0',
params: {
channelId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'YouTube channel ID starting with "UC" (24-character string, use either channelId or username)',
},
username: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'YouTube channel username (use either channelId or username)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeChannelInfoParams) => {
let url =
'https://www.googleapis.com/youtube/v3/channels?part=snippet,statistics,contentDetails,brandingSettings'
if (params.channelId) {
url += `&id=${encodeURIComponent(params.channelId)}`
} else if (params.username) {
url += `&forUsername=${encodeURIComponent(params.username)}`
}
url += `&key=${params.apiKey}`
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeChannelInfoResponse> => {
const data = await response.json()
if (!data.items || data.items.length === 0) {
return {
success: false,
output: {
channelId: '',
title: '',
description: '',
subscriberCount: 0,
videoCount: 0,
viewCount: 0,
publishedAt: '',
thumbnail: '',
customUrl: null,
country: null,
uploadsPlaylistId: null,
bannerImageUrl: null,
hiddenSubscriberCount: false,
},
error: 'Channel not found',
}
}
const item = data.items[0]
return {
success: true,
output: {
channelId: item.id ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
subscriberCount: Number(item.statistics?.subscriberCount || 0),
videoCount: Number(item.statistics?.videoCount || 0),
viewCount: Number(item.statistics?.viewCount || 0),
publishedAt: item.snippet?.publishedAt ?? '',
thumbnail:
item.snippet?.thumbnails?.high?.url ||
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.default?.url ||
'',
customUrl: item.snippet?.customUrl ?? null,
country: item.snippet?.country ?? null,
uploadsPlaylistId: item.contentDetails?.relatedPlaylists?.uploads ?? null,
bannerImageUrl: item.brandingSettings?.image?.bannerExternalUrl ?? null,
hiddenSubscriberCount: item.statistics?.hiddenSubscriberCount ?? false,
},
}
},
outputs: {
channelId: {
type: 'string',
description: 'YouTube channel ID',
},
title: {
type: 'string',
description: 'Channel name',
},
description: {
type: 'string',
description: 'Channel description',
},
subscriberCount: {
type: 'number',
description: 'Number of subscribers (0 if hidden)',
},
videoCount: {
type: 'number',
description: 'Number of public videos',
},
viewCount: {
type: 'number',
description: 'Total channel views',
},
publishedAt: {
type: 'string',
description: 'Channel creation date',
},
thumbnail: {
type: 'string',
description: 'Channel thumbnail/avatar URL',
},
customUrl: {
type: 'string',
description: 'Channel custom URL (handle)',
optional: true,
},
country: {
type: 'string',
description: 'Country the channel is associated with',
optional: true,
},
uploadsPlaylistId: {
type: 'string',
description: 'Playlist ID containing all channel uploads (use with playlist_items)',
optional: true,
},
bannerImageUrl: {
type: 'string',
description: 'Channel banner image URL',
optional: true,
},
hiddenSubscriberCount: {
type: 'boolean',
description: 'Whether the subscriber count is hidden',
},
},
}
+138
View File
@@ -0,0 +1,138 @@
import type { ToolConfig } from '@/tools/types'
import type {
YouTubeChannelPlaylistsParams,
YouTubeChannelPlaylistsResponse,
} from '@/tools/youtube/types'
export const youtubeChannelPlaylistsTool: ToolConfig<
YouTubeChannelPlaylistsParams,
YouTubeChannelPlaylistsResponse
> = {
id: 'youtube_channel_playlists',
name: 'YouTube Channel Playlists',
description: 'Get all public playlists from a specific YouTube channel.',
version: '1.1.0',
params: {
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'YouTube channel ID starting with "UC" (24-character string) to get playlists from',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 10,
description: 'Maximum number of playlists to return (1-50)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination (from previous response nextPageToken)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeChannelPlaylistsParams) => {
let url = `https://www.googleapis.com/youtube/v3/playlists?part=snippet,contentDetails&channelId=${encodeURIComponent(
params.channelId
)}&key=${params.apiKey}`
url += `&maxResults=${Number(params.maxResults || 10)}`
if (params.pageToken) {
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeChannelPlaylistsResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
error: data.error.message || 'Failed to fetch channel playlists',
}
}
if (!data.items || data.items.length === 0) {
return {
success: true,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
}
}
const items = (data.items || []).map((item: any) => ({
playlistId: item.id ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
thumbnail:
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.default?.url ||
item.snippet?.thumbnails?.high?.url ||
'',
itemCount: Number(item.contentDetails?.itemCount || 0),
publishedAt: item.snippet?.publishedAt ?? '',
channelTitle: item.snippet?.channelTitle ?? '',
}))
return {
success: true,
output: {
items,
totalResults: data.pageInfo?.totalResults || items.length,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of playlists from the channel',
items: {
type: 'object',
properties: {
playlistId: { type: 'string', description: 'YouTube playlist ID' },
title: { type: 'string', description: 'Playlist title' },
description: { type: 'string', description: 'Playlist description' },
thumbnail: { type: 'string', description: 'Playlist thumbnail URL' },
itemCount: { type: 'number', description: 'Number of videos in playlist' },
publishedAt: { type: 'string', description: 'Playlist creation date' },
channelTitle: { type: 'string', description: 'Channel name' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of playlists in the channel',
},
nextPageToken: {
type: 'string',
description: 'Token for accessing the next page of results',
optional: true,
},
},
}
+133
View File
@@ -0,0 +1,133 @@
import type { ToolConfig } from '@/tools/types'
import type {
YouTubeChannelVideosParams,
YouTubeChannelVideosResponse,
} from '@/tools/youtube/types'
export const youtubeChannelVideosTool: ToolConfig<
YouTubeChannelVideosParams,
YouTubeChannelVideosResponse
> = {
id: 'youtube_channel_videos',
name: 'YouTube Channel Videos',
description:
'Search for videos from a specific YouTube channel with sorting options. For complete channel video list, use channel_info to get uploadsPlaylistId, then use playlist_items.',
version: '1.1.0',
params: {
channelId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'YouTube channel ID starting with "UC" (24-character string) to get videos from',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 10,
description: 'Maximum number of videos to return (1-50)',
},
order: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort order: "date" (newest first, default), "rating", "relevance", "title", "viewCount"',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination (from previous response nextPageToken)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeChannelVideosParams) => {
let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&channelId=${encodeURIComponent(
params.channelId
)}&key=${params.apiKey}`
url += `&maxResults=${Number(params.maxResults || 10)}`
url += `&order=${params.order || 'date'}`
if (params.pageToken) {
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeChannelVideosResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
error: data.error.message || 'Failed to fetch channel videos',
}
}
const items = (data.items || []).map((item: any) => ({
videoId: item.id?.videoId ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
thumbnail:
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.default?.url ||
item.snippet?.thumbnails?.high?.url ||
'',
publishedAt: item.snippet?.publishedAt ?? '',
channelTitle: item.snippet?.channelTitle ?? '',
}))
return {
success: true,
output: {
items,
totalResults: data.pageInfo?.totalResults || items.length,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of videos from the channel',
items: {
type: 'object',
properties: {
videoId: { type: 'string', description: 'YouTube video ID' },
title: { type: 'string', description: 'Video title' },
description: { type: 'string', description: 'Video description' },
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
publishedAt: { type: 'string', description: 'Video publish date' },
channelTitle: { type: 'string', description: 'Channel name' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of videos in the channel',
},
nextPageToken: {
type: 'string',
description: 'Token for accessing the next page of results',
optional: true,
},
},
}
+134
View File
@@ -0,0 +1,134 @@
import type { ToolConfig } from '@/tools/types'
import type { YouTubeCommentsParams, YouTubeCommentsResponse } from '@/tools/youtube/types'
export const youtubeCommentsTool: ToolConfig<YouTubeCommentsParams, YouTubeCommentsResponse> = {
id: 'youtube_comments',
name: 'YouTube Video Comments',
description: 'Get top-level comments from a YouTube video with author details and engagement.',
version: '1.1.0',
params: {
videoId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'YouTube video ID (11-character string, e.g., "dQw4w9WgXcQ")',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 20,
description: 'Maximum number of comments to return (1-100)',
},
order: {
type: 'string',
required: false,
visibility: 'user-or-llm',
default: 'relevance',
description: 'Order of comments: "time" (newest first) or "relevance" (most relevant first)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination (from previous response nextPageToken)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeCommentsParams) => {
let url = `https://www.googleapis.com/youtube/v3/commentThreads?part=snippet,replies&videoId=${encodeURIComponent(params.videoId)}&key=${params.apiKey}`
url += `&maxResults=${Number(params.maxResults || 20)}`
url += `&order=${params.order || 'relevance'}`
if (params.pageToken) {
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeCommentsResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
error: data.error.message || 'Failed to fetch comments',
}
}
const items = (data.items || []).map((item: any) => {
const topLevelComment = item.snippet?.topLevelComment?.snippet
return {
commentId: item.snippet?.topLevelComment?.id ?? item.id ?? '',
authorDisplayName: topLevelComment?.authorDisplayName ?? '',
authorChannelUrl: topLevelComment?.authorChannelUrl ?? '',
authorProfileImageUrl: topLevelComment?.authorProfileImageUrl ?? '',
textDisplay: topLevelComment?.textDisplay ?? '',
textOriginal: topLevelComment?.textOriginal ?? '',
likeCount: Number(topLevelComment?.likeCount || 0),
publishedAt: topLevelComment?.publishedAt ?? '',
updatedAt: topLevelComment?.updatedAt ?? '',
replyCount: Number(item.snippet?.totalReplyCount || 0),
}
})
return {
success: true,
output: {
items,
totalResults: data.pageInfo?.totalResults || items.length,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of top-level comments from the video',
items: {
type: 'object',
properties: {
commentId: { type: 'string', description: 'Comment ID' },
authorDisplayName: { type: 'string', description: 'Comment author display name' },
authorChannelUrl: { type: 'string', description: 'Comment author channel URL' },
authorProfileImageUrl: {
type: 'string',
description: 'Comment author profile image URL',
},
textDisplay: { type: 'string', description: 'Comment text (HTML formatted)' },
textOriginal: { type: 'string', description: 'Comment text (plain text)' },
likeCount: { type: 'number', description: 'Number of likes on the comment' },
publishedAt: { type: 'string', description: 'When the comment was posted' },
updatedAt: { type: 'string', description: 'When the comment was last edited' },
replyCount: { type: 'number', description: 'Number of replies to this comment' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of comment threads available',
},
nextPageToken: {
type: 'string',
description: 'Token for accessing the next page of results',
optional: true,
},
},
}
+19
View File
@@ -0,0 +1,19 @@
import { youtubeChannelInfoTool } from '@/tools/youtube/channel_info'
import { youtubeChannelPlaylistsTool } from '@/tools/youtube/channel_playlists'
import { youtubeChannelVideosTool } from '@/tools/youtube/channel_videos'
import { youtubeCommentsTool } from '@/tools/youtube/comments'
import { youtubePlaylistItemsTool } from '@/tools/youtube/playlist_items'
import { youtubeSearchTool } from '@/tools/youtube/search'
import { youtubeTrendingTool } from '@/tools/youtube/trending'
import { youtubeVideoCategoriesTool } from '@/tools/youtube/video_categories'
import { youtubeVideoDetailsTool } from '@/tools/youtube/video_details'
export { youtubeSearchTool }
export { youtubeVideoDetailsTool }
export { youtubeChannelInfoTool }
export { youtubePlaylistItemsTool }
export { youtubeCommentsTool }
export { youtubeChannelVideosTool }
export { youtubeChannelPlaylistsTool }
export { youtubeTrendingTool }
export { youtubeVideoCategoriesTool }
+138
View File
@@ -0,0 +1,138 @@
import type { ToolConfig } from '@/tools/types'
import type {
YouTubePlaylistItemsParams,
YouTubePlaylistItemsResponse,
} from '@/tools/youtube/types'
export const youtubePlaylistItemsTool: ToolConfig<
YouTubePlaylistItemsParams,
YouTubePlaylistItemsResponse
> = {
id: 'youtube_playlist_items',
name: 'YouTube Playlist Items',
description:
'Get videos from a YouTube playlist. Can be used with a channel uploads playlist to get all channel videos.',
version: '1.1.0',
params: {
playlistId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'YouTube playlist ID starting with "PL" or "UU" (34-character string). Use uploadsPlaylistId from channel_info to get all channel videos.',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 10,
description: 'Maximum number of videos to return (1-50)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination (from previous response nextPageToken)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubePlaylistItemsParams) => {
let url = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=${encodeURIComponent(params.playlistId)}&key=${params.apiKey}`
url += `&maxResults=${Number(params.maxResults || 10)}`
if (params.pageToken) {
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubePlaylistItemsResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
error: data.error.message || 'Failed to fetch playlist items',
}
}
const items = (data.items || []).map((item: any, index: number) => ({
videoId: item.contentDetails?.videoId ?? item.snippet?.resourceId?.videoId ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
thumbnail:
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.default?.url ||
item.snippet?.thumbnails?.high?.url ||
'',
publishedAt: item.snippet?.publishedAt ?? '',
channelTitle: item.snippet?.channelTitle ?? '',
position: item.snippet?.position ?? index,
videoOwnerChannelId: item.snippet?.videoOwnerChannelId ?? null,
videoOwnerChannelTitle: item.snippet?.videoOwnerChannelTitle ?? null,
}))
return {
success: true,
output: {
items,
totalResults: data.pageInfo?.totalResults || items.length,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of videos in the playlist',
items: {
type: 'object',
properties: {
videoId: { type: 'string', description: 'YouTube video ID' },
title: { type: 'string', description: 'Video title' },
description: { type: 'string', description: 'Video description' },
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
publishedAt: { type: 'string', description: 'Date added to playlist' },
channelTitle: { type: 'string', description: 'Playlist owner channel name' },
position: { type: 'number', description: 'Position in playlist (0-indexed)' },
videoOwnerChannelId: {
type: 'string',
description: 'Channel ID of the video owner',
optional: true,
},
videoOwnerChannelTitle: {
type: 'string',
description: 'Channel name of the video owner',
optional: true,
},
},
},
},
totalResults: {
type: 'number',
description: 'Total number of items in playlist',
},
nextPageToken: {
type: 'string',
description: 'Token for accessing the next page of results',
optional: true,
},
},
}
+244
View File
@@ -0,0 +1,244 @@
import type { ToolConfig } from '@/tools/types'
import type { YouTubeSearchParams, YouTubeSearchResponse } from '@/tools/youtube/types'
export const youtubeSearchTool: ToolConfig<YouTubeSearchParams, YouTubeSearchResponse> = {
id: 'youtube_search',
name: 'YouTube Search',
description:
'Search for videos on YouTube using the YouTube Data API. Supports advanced filtering by channel, date range, duration, category, quality, captions, live streams, and more.',
version: '1.2.0',
params: {
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search query for YouTube videos',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 5,
description: 'Maximum number of videos to return (1-50)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination (from previous response nextPageToken)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
channelId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter results to a specific YouTube channel ID starting with "UC" (24-character string)',
},
publishedAfter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return videos published after this date (RFC 3339 format: "2024-01-01T00:00:00Z")',
},
publishedBefore: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Only return videos published before this date (RFC 3339 format: "2024-01-01T00:00:00Z")',
},
videoDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by video length: "short" (<4 min), "medium" (4-20 min), "long" (>20 min), "any"',
},
order: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Sort results by: "date", "rating", "relevance" (default), "title", "videoCount", "viewCount"',
},
videoCategoryId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by YouTube category ID (e.g., "10" for Music, "20" for Gaming). Use video_categories to list IDs.',
},
videoDefinition: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by video quality: "high" (HD), "standard", "any"',
},
videoCaption: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by caption availability: "closedCaption" (has captions), "none" (no captions), "any"',
},
eventType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by live broadcast status: "live" (currently live), "upcoming" (scheduled), "completed" (past streams)',
},
regionCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Return results relevant to a specific region (ISO 3166-1 alpha-2 country code, e.g., "US", "GB")',
},
relevanceLanguage: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Return results most relevant to a language (ISO 639-1 code, e.g., "en", "es")',
},
safeSearch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Content filtering level: "moderate" (default), "none", "strict"',
},
},
request: {
url: (params: YouTubeSearchParams) => {
let url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&key=${params.apiKey}&q=${encodeURIComponent(
params.query
)}`
url += `&maxResults=${Number(params.maxResults || 5)}`
if (params.pageToken) {
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
}
if (params.channelId) {
url += `&channelId=${encodeURIComponent(params.channelId)}`
}
if (params.publishedAfter) {
url += `&publishedAfter=${encodeURIComponent(params.publishedAfter)}`
}
if (params.publishedBefore) {
url += `&publishedBefore=${encodeURIComponent(params.publishedBefore)}`
}
if (params.videoDuration) {
url += `&videoDuration=${params.videoDuration}`
}
if (params.order) {
url += `&order=${params.order}`
}
if (params.videoCategoryId) {
url += `&videoCategoryId=${params.videoCategoryId}`
}
if (params.videoDefinition) {
url += `&videoDefinition=${params.videoDefinition}`
}
if (params.videoCaption) {
url += `&videoCaption=${params.videoCaption}`
}
if (params.eventType) {
url += `&eventType=${params.eventType}`
}
if (params.regionCode) {
url += `&regionCode=${params.regionCode}`
}
if (params.relevanceLanguage) {
url += `&relevanceLanguage=${params.relevanceLanguage}`
}
if (params.safeSearch) {
url += `&safeSearch=${params.safeSearch}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeSearchResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
error: data.error.message || 'Search failed',
}
}
const items = (data.items || []).map((item: any) => ({
videoId: item.id?.videoId ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
thumbnail:
item.snippet?.thumbnails?.default?.url ||
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.high?.url ||
'',
channelId: item.snippet?.channelId ?? '',
channelTitle: item.snippet?.channelTitle ?? '',
publishedAt: item.snippet?.publishedAt ?? '',
liveBroadcastContent: item.snippet?.liveBroadcastContent ?? 'none',
}))
return {
success: true,
output: {
items,
totalResults: data.pageInfo?.totalResults || 0,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of YouTube videos matching the search query',
items: {
type: 'object',
properties: {
videoId: { type: 'string', description: 'YouTube video ID' },
title: { type: 'string', description: 'Video title' },
description: { type: 'string', description: 'Video description' },
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
channelId: { type: 'string', description: 'Channel ID that uploaded the video' },
channelTitle: { type: 'string', description: 'Channel name' },
publishedAt: { type: 'string', description: 'Video publish date' },
liveBroadcastContent: {
type: 'string',
description: 'Live broadcast status: "none", "live", or "upcoming"',
},
},
},
},
totalResults: {
type: 'number',
description: 'Total number of search results available',
},
nextPageToken: {
type: 'string',
description: 'Token for accessing the next page of results',
optional: true,
},
},
}
+139
View File
@@ -0,0 +1,139 @@
import type { ToolConfig } from '@/tools/types'
import type { YouTubeTrendingParams, YouTubeTrendingResponse } from '@/tools/youtube/types'
export const youtubeTrendingTool: ToolConfig<YouTubeTrendingParams, YouTubeTrendingResponse> = {
id: 'youtube_trending',
name: 'YouTube Trending Videos',
description:
'Get the most popular/trending videos on YouTube. Can filter by region and video category.',
version: '1.0.0',
params: {
regionCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'ISO 3166-1 alpha-2 country code to get trending videos for (e.g., "US", "GB", "JP"). Defaults to US.',
},
videoCategoryId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Filter by video category ID (e.g., "10" for Music, "20" for Gaming, "17" for Sports)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
default: 10,
description: 'Maximum number of trending videos to return (1-50)',
},
pageToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Page token for pagination (from previous response nextPageToken)',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeTrendingParams) => {
let url = `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails&chart=mostPopular&key=${params.apiKey}`
url += `&maxResults=${Number(params.maxResults || 10)}`
url += `&regionCode=${params.regionCode || 'US'}`
if (params.videoCategoryId) {
url += `&videoCategoryId=${params.videoCategoryId}`
}
if (params.pageToken) {
url += `&pageToken=${encodeURIComponent(params.pageToken)}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeTrendingResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
nextPageToken: null,
},
error: data.error.message || 'Failed to fetch trending videos',
}
}
const items = (data.items || []).map((item: any) => ({
videoId: item.id ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
thumbnail:
item.snippet?.thumbnails?.high?.url ||
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.default?.url ||
'',
channelId: item.snippet?.channelId ?? '',
channelTitle: item.snippet?.channelTitle ?? '',
publishedAt: item.snippet?.publishedAt ?? '',
viewCount: Number(item.statistics?.viewCount || 0),
likeCount: Number(item.statistics?.likeCount || 0),
commentCount: Number(item.statistics?.commentCount || 0),
duration: item.contentDetails?.duration ?? '',
}))
return {
success: true,
output: {
items,
totalResults: data.pageInfo?.totalResults || items.length,
nextPageToken: data.nextPageToken ?? null,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of trending videos',
items: {
type: 'object',
properties: {
videoId: { type: 'string', description: 'YouTube video ID' },
title: { type: 'string', description: 'Video title' },
description: { type: 'string', description: 'Video description' },
thumbnail: { type: 'string', description: 'Video thumbnail URL' },
channelId: { type: 'string', description: 'Channel ID' },
channelTitle: { type: 'string', description: 'Channel name' },
publishedAt: { type: 'string', description: 'Video publish date' },
viewCount: { type: 'number', description: 'Number of views' },
likeCount: { type: 'number', description: 'Number of likes' },
commentCount: { type: 'number', description: 'Number of comments' },
duration: { type: 'string', description: 'Video duration in ISO 8601 format' },
},
},
},
totalResults: {
type: 'number',
description: 'Total number of trending videos available',
},
nextPageToken: {
type: 'string',
description: 'Token for accessing the next page of results',
optional: true,
},
},
}
+253
View File
@@ -0,0 +1,253 @@
import type { ToolResponse } from '@/tools/types'
export interface YouTubeSearchParams {
apiKey: string
query: string
maxResults?: number
pageToken?: string
channelId?: string
publishedAfter?: string
publishedBefore?: string
videoDuration?: 'any' | 'short' | 'medium' | 'long'
order?: 'date' | 'rating' | 'relevance' | 'title' | 'videoCount' | 'viewCount'
videoCategoryId?: string
videoDefinition?: 'any' | 'high' | 'standard'
videoCaption?: 'any' | 'closedCaption' | 'none'
regionCode?: string
relevanceLanguage?: string
safeSearch?: 'moderate' | 'none' | 'strict'
eventType?: 'completed' | 'live' | 'upcoming'
}
export interface YouTubeSearchResponse extends ToolResponse {
output: {
items: Array<{
videoId: string
title: string
description: string
thumbnail: string
channelId: string
channelTitle: string
publishedAt: string
liveBroadcastContent: string
}>
totalResults: number
nextPageToken?: string | null
}
}
export interface YouTubeVideoDetailsParams {
apiKey: string
videoId: string
}
export interface YouTubeVideoDetailsResponse extends ToolResponse {
output: {
videoId: string
title: string
description: string
channelId: string
channelTitle: string
publishedAt: string
duration: string
viewCount: number
likeCount: number
commentCount: number
favoriteCount: number
thumbnail: string
tags: string[]
categoryId: string | null
definition: string | null
caption: string | null
licensedContent: boolean | null
privacyStatus: string | null
liveBroadcastContent: string | null
defaultLanguage: string | null
defaultAudioLanguage: string | null
// Live streaming details
isLiveContent: boolean
scheduledStartTime: string | null
actualStartTime: string | null
actualEndTime: string | null
concurrentViewers: number | null
activeLiveChatId: string | null
}
}
export interface YouTubeChannelInfoParams {
apiKey: string
channelId?: string
username?: string
}
export interface YouTubeChannelInfoResponse extends ToolResponse {
output: {
channelId: string
title: string
description: string
subscriberCount: number
videoCount: number
viewCount: number
publishedAt: string
thumbnail: string
customUrl: string | null
country: string | null
uploadsPlaylistId: string | null
bannerImageUrl: string | null
hiddenSubscriberCount: boolean
}
}
export interface YouTubePlaylistItemsParams {
apiKey: string
playlistId: string
maxResults?: number
pageToken?: string
}
export interface YouTubePlaylistItemsResponse extends ToolResponse {
output: {
items: Array<{
videoId: string
title: string
description: string
thumbnail: string
publishedAt: string
channelTitle: string
position: number
videoOwnerChannelId: string | null
videoOwnerChannelTitle: string | null
}>
totalResults: number
nextPageToken?: string | null
}
}
export interface YouTubeCommentsParams {
apiKey: string
videoId: string
maxResults?: number
order?: 'time' | 'relevance'
pageToken?: string
}
export interface YouTubeCommentsResponse extends ToolResponse {
output: {
items: Array<{
commentId: string
authorDisplayName: string
authorChannelUrl: string
authorProfileImageUrl: string
textDisplay: string
textOriginal: string
likeCount: number
publishedAt: string
updatedAt: string
replyCount: number
}>
totalResults: number
nextPageToken?: string | null
}
}
export interface YouTubeChannelVideosParams {
apiKey: string
channelId: string
maxResults?: number
order?: 'date' | 'rating' | 'relevance' | 'title' | 'viewCount'
pageToken?: string
}
export interface YouTubeChannelVideosResponse extends ToolResponse {
output: {
items: Array<{
videoId: string
title: string
description: string
thumbnail: string
publishedAt: string
channelTitle: string
}>
totalResults: number
nextPageToken?: string | null
}
}
export interface YouTubeChannelPlaylistsParams {
apiKey: string
channelId: string
maxResults?: number
pageToken?: string
}
export interface YouTubeChannelPlaylistsResponse extends ToolResponse {
output: {
items: Array<{
playlistId: string
title: string
description: string
thumbnail: string
itemCount: number
publishedAt: string
channelTitle: string
}>
totalResults: number
nextPageToken?: string | null
}
}
export interface YouTubeTrendingParams {
apiKey: string
regionCode?: string
videoCategoryId?: string
maxResults?: number
pageToken?: string
}
export interface YouTubeTrendingResponse extends ToolResponse {
output: {
items: Array<{
videoId: string
title: string
description: string
thumbnail: string
channelId: string
channelTitle: string
publishedAt: string
viewCount: number
likeCount: number
commentCount: number
duration: string
}>
totalResults: number
nextPageToken?: string | null
}
}
export interface YouTubeVideoCategoriesParams {
apiKey: string
regionCode?: string
hl?: string
}
export interface YouTubeVideoCategoriesResponse extends ToolResponse {
output: {
items: Array<{
categoryId: string
title: string
assignable: boolean
}>
totalResults: number
}
}
export type YouTubeResponse =
| YouTubeSearchResponse
| YouTubeVideoDetailsResponse
| YouTubeChannelInfoResponse
| YouTubePlaylistItemsResponse
| YouTubeCommentsResponse
| YouTubeChannelVideosResponse
| YouTubeChannelPlaylistsResponse
| YouTubeTrendingResponse
| YouTubeVideoCategoriesResponse
+109
View File
@@ -0,0 +1,109 @@
import type { ToolConfig } from '@/tools/types'
import type {
YouTubeVideoCategoriesParams,
YouTubeVideoCategoriesResponse,
} from '@/tools/youtube/types'
export const youtubeVideoCategoriesTool: ToolConfig<
YouTubeVideoCategoriesParams,
YouTubeVideoCategoriesResponse
> = {
id: 'youtube_video_categories',
name: 'YouTube Video Categories',
description:
'Get a list of video categories available on YouTube. Use this to discover valid category IDs for filtering search and trending results.',
version: '1.0.0',
params: {
regionCode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'ISO 3166-1 alpha-2 country code to get categories for (e.g., "US", "GB", "JP"). Defaults to US.',
},
hl: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Language for category titles (ISO 639-1 code, e.g., "en", "es", "fr"). Defaults to English.',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeVideoCategoriesParams) => {
let url = `https://www.googleapis.com/youtube/v3/videoCategories?part=snippet&key=${params.apiKey}`
url += `&regionCode=${params.regionCode || 'US'}`
if (params.hl) {
url += `&hl=${params.hl}`
}
return url
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeVideoCategoriesResponse> => {
const data = await response.json()
if (data.error) {
return {
success: false,
output: {
items: [],
totalResults: 0,
},
error: data.error.message || 'Failed to fetch video categories',
}
}
const items = (data.items || [])
.filter((item: any) => item.snippet?.assignable !== false)
.map((item: any) => ({
categoryId: item.id ?? '',
title: item.snippet?.title ?? '',
assignable: item.snippet?.assignable ?? false,
}))
return {
success: true,
output: {
items,
totalResults: items.length,
},
}
},
outputs: {
items: {
type: 'array',
description: 'Array of video categories available in the specified region',
items: {
type: 'object',
properties: {
categoryId: {
type: 'string',
description: 'Category ID to use in search/trending filters (e.g., "10" for Music)',
},
title: { type: 'string', description: 'Human-readable category name' },
assignable: {
type: 'boolean',
description: 'Whether videos can be tagged with this category',
},
},
},
},
totalResults: {
type: 'number',
description: 'Total number of categories available',
},
},
}
+247
View File
@@ -0,0 +1,247 @@
import type { ToolConfig } from '@/tools/types'
import type { YouTubeVideoDetailsParams, YouTubeVideoDetailsResponse } from '@/tools/youtube/types'
export const youtubeVideoDetailsTool: ToolConfig<
YouTubeVideoDetailsParams,
YouTubeVideoDetailsResponse
> = {
id: 'youtube_video_details',
name: 'YouTube Video Details',
description:
'Get detailed information about a specific YouTube video including statistics, content details, live streaming info, and metadata.',
version: '1.2.0',
params: {
videoId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'YouTube video ID (11-character string, e.g., "dQw4w9WgXcQ")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'YouTube API Key',
},
},
request: {
url: (params: YouTubeVideoDetailsParams) => {
return `https://www.googleapis.com/youtube/v3/videos?part=snippet,statistics,contentDetails,status,liveStreamingDetails&id=${encodeURIComponent(params.videoId)}&key=${params.apiKey}`
},
method: 'GET',
headers: () => ({
'Content-Type': 'application/json',
}),
},
transformResponse: async (response: Response): Promise<YouTubeVideoDetailsResponse> => {
const data = await response.json()
if (!data.items || data.items.length === 0) {
return {
success: false,
output: {
videoId: '',
title: '',
description: '',
channelId: '',
channelTitle: '',
publishedAt: '',
duration: '',
viewCount: 0,
likeCount: 0,
commentCount: 0,
favoriteCount: 0,
thumbnail: '',
tags: [],
categoryId: null,
definition: null,
caption: null,
licensedContent: null,
privacyStatus: null,
liveBroadcastContent: null,
defaultLanguage: null,
defaultAudioLanguage: null,
isLiveContent: false,
scheduledStartTime: null,
actualStartTime: null,
actualEndTime: null,
concurrentViewers: null,
activeLiveChatId: null,
},
error: 'Video not found',
}
}
const item = data.items[0]
const liveDetails = item.liveStreamingDetails
return {
success: true,
output: {
videoId: item.id ?? '',
title: item.snippet?.title ?? '',
description: item.snippet?.description ?? '',
channelId: item.snippet?.channelId ?? '',
channelTitle: item.snippet?.channelTitle ?? '',
publishedAt: item.snippet?.publishedAt ?? '',
duration: item.contentDetails?.duration ?? '',
viewCount: Number(item.statistics?.viewCount || 0),
likeCount: Number(item.statistics?.likeCount || 0),
commentCount: Number(item.statistics?.commentCount || 0),
favoriteCount: Number(item.statistics?.favoriteCount || 0),
thumbnail:
item.snippet?.thumbnails?.high?.url ||
item.snippet?.thumbnails?.medium?.url ||
item.snippet?.thumbnails?.default?.url ||
'',
tags: item.snippet?.tags ?? [],
categoryId: item.snippet?.categoryId ?? null,
definition: item.contentDetails?.definition ?? null,
caption: item.contentDetails?.caption ?? null,
licensedContent: item.contentDetails?.licensedContent ?? null,
privacyStatus: item.status?.privacyStatus ?? null,
liveBroadcastContent: item.snippet?.liveBroadcastContent ?? null,
defaultLanguage: item.snippet?.defaultLanguage ?? null,
defaultAudioLanguage: item.snippet?.defaultAudioLanguage ?? null,
// Live streaming details
isLiveContent: liveDetails !== undefined,
scheduledStartTime: liveDetails?.scheduledStartTime ?? null,
actualStartTime: liveDetails?.actualStartTime ?? null,
actualEndTime: liveDetails?.actualEndTime ?? null,
concurrentViewers: liveDetails?.concurrentViewers
? Number(liveDetails.concurrentViewers)
: null,
activeLiveChatId: liveDetails?.activeLiveChatId ?? null,
},
}
},
outputs: {
videoId: {
type: 'string',
description: 'YouTube video ID',
},
title: {
type: 'string',
description: 'Video title',
},
description: {
type: 'string',
description: 'Video description',
},
channelId: {
type: 'string',
description: 'Channel ID',
},
channelTitle: {
type: 'string',
description: 'Channel name',
},
publishedAt: {
type: 'string',
description: 'Published date and time',
},
duration: {
type: 'string',
description: 'Video duration in ISO 8601 format (e.g., "PT4M13S" for 4 min 13 sec)',
},
viewCount: {
type: 'number',
description: 'Number of views',
},
likeCount: {
type: 'number',
description: 'Number of likes',
},
commentCount: {
type: 'number',
description: 'Number of comments',
},
favoriteCount: {
type: 'number',
description: 'Number of times added to favorites',
},
thumbnail: {
type: 'string',
description: 'Video thumbnail URL',
},
tags: {
type: 'array',
description: 'Video tags',
items: {
type: 'string',
},
},
categoryId: {
type: 'string',
description: 'YouTube video category ID',
optional: true,
},
definition: {
type: 'string',
description: 'Video definition: "hd" or "sd"',
optional: true,
},
caption: {
type: 'string',
description: 'Whether captions are available: "true" or "false"',
optional: true,
},
licensedContent: {
type: 'boolean',
description: 'Whether the video is licensed content',
optional: true,
},
privacyStatus: {
type: 'string',
description: 'Video privacy status: "public", "private", or "unlisted"',
optional: true,
},
liveBroadcastContent: {
type: 'string',
description: 'Live broadcast status: "live", "upcoming", or "none"',
optional: true,
},
defaultLanguage: {
type: 'string',
description: 'Default language of the video metadata',
optional: true,
},
defaultAudioLanguage: {
type: 'string',
description: 'Default audio language of the video',
optional: true,
},
isLiveContent: {
type: 'boolean',
description: 'Whether this video is or was a live stream',
},
scheduledStartTime: {
type: 'string',
description: 'Scheduled start time for upcoming live streams (ISO 8601)',
optional: true,
},
actualStartTime: {
type: 'string',
description: 'When the live stream actually started (ISO 8601)',
optional: true,
},
actualEndTime: {
type: 'string',
description: 'When the live stream ended (ISO 8601)',
optional: true,
},
concurrentViewers: {
type: 'number',
description: 'Current number of viewers (only for active live streams)',
optional: true,
},
activeLiveChatId: {
type: 'string',
description: 'Live chat ID for the stream (only for active live streams)',
optional: true,
},
},
}