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
+79
View File
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XCreateBookmarkParams, XCreateBookmarkResponse } from '@/tools/x/types'
const logger = createLogger('XCreateBookmarkTool')
export const xCreateBookmarkTool: ToolConfig<XCreateBookmarkParams, XCreateBookmarkResponse> = {
id: 'x_create_bookmark',
name: 'X Create Bookmark',
description: 'Bookmark a tweet for the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to bookmark',
},
},
request: {
url: (params) => `https://api.x.com/2/users/${params.userId.trim()}/bookmarks`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
tweet_id: params.tweetId.trim(),
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Create Bookmark API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to bookmark tweet',
output: {
bookmarked: false,
},
}
}
return {
success: true,
output: {
bookmarked: data.data.bookmarked ?? false,
},
}
},
outputs: {
bookmarked: {
type: 'boolean',
description: 'Whether the tweet was successfully bookmarked',
},
},
}
+129
View File
@@ -0,0 +1,129 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XCreateTweetParams, XCreateTweetResponse } from '@/tools/x/types'
const logger = createLogger('XCreateTweetTool')
export const xCreateTweetTool: ToolConfig<XCreateTweetParams, XCreateTweetResponse> = {
id: 'x_create_tweet',
name: 'X Create Tweet',
description: 'Create a new tweet, reply, or quote tweet on X',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text content of the tweet (max 280 characters)',
},
replyToTweetId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Tweet ID to reply to',
},
quoteTweetId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Tweet ID to quote',
},
mediaIds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated media IDs to attach (up to 4)',
},
replySettings: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Who can reply: "mentionedUsers", "following", "subscribers", or "verified"',
},
},
request: {
url: 'https://api.x.com/2/tweets',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = {
text: params.text,
}
if (params.replyToTweetId) {
body.reply = { in_reply_to_tweet_id: params.replyToTweetId.trim() }
}
if (params.quoteTweetId) {
body.quote_tweet_id = params.quoteTweetId.trim()
}
if (params.mediaIds) {
const ids = params.mediaIds
.split(',')
.map((id) => id.trim())
.filter(Boolean)
if (ids.length > 0) {
body.media = { media_ids: ids }
}
}
if (params.replySettings) {
body.reply_settings = params.replySettings
}
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Create Tweet API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to create tweet',
output: {
id: '',
text: '',
},
}
}
return {
success: true,
output: {
id: data.data.id ?? '',
text: data.data.text ?? '',
},
}
},
outputs: {
id: {
type: 'string',
description: 'The ID of the created tweet',
},
text: {
type: 'string',
description: 'The text of the created tweet',
},
},
}
+77
View File
@@ -0,0 +1,77 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XDeleteBookmarkParams, XDeleteBookmarkResponse } from '@/tools/x/types'
const logger = createLogger('XDeleteBookmarkTool')
export const xDeleteBookmarkTool: ToolConfig<XDeleteBookmarkParams, XDeleteBookmarkResponse> = {
id: 'x_delete_bookmark',
name: 'X Delete Bookmark',
description: "Remove a tweet from the authenticated user's bookmarks",
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to remove from bookmarks',
},
},
request: {
url: (params) =>
`https://api.x.com/2/users/${params.userId.trim()}/bookmarks/${params.tweetId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Delete Bookmark API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to remove bookmark',
output: {
bookmarked: false,
},
}
}
return {
success: true,
output: {
bookmarked: data.data.bookmarked ?? false,
},
}
},
outputs: {
bookmarked: {
type: 'boolean',
description: 'Whether the tweet is still bookmarked (should be false after deletion)',
},
},
}
+70
View File
@@ -0,0 +1,70 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XDeleteTweetParams, XDeleteTweetResponse } from '@/tools/x/types'
const logger = createLogger('XDeleteTweetTool')
export const xDeleteTweetTool: ToolConfig<XDeleteTweetParams, XDeleteTweetResponse> = {
id: 'x_delete_tweet',
name: 'X Delete Tweet',
description: 'Delete a tweet authored by the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the tweet to delete',
},
},
request: {
url: (params) => `https://api.x.com/2/tweets/${params.tweetId.trim()}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Delete Tweet API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to delete tweet',
output: {
deleted: false,
},
}
}
return {
success: true,
output: {
deleted: data.data.deleted ?? false,
},
}
},
outputs: {
deleted: {
type: 'boolean',
description: 'Whether the tweet was successfully deleted',
},
},
}
+128
View File
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetBlockingParams, XUserListResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XGetBlockingTool')
export const xGetBlockingTool: ToolConfig<XGetBlockingParams, XUserListResponse> = {
id: 'x_get_blocking',
name: 'X Get Blocking',
description: 'Get the list of users blocked by the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-1000)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'user.fields': 'created_at,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(1000, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/users/${params.userId.trim()}/blocking?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Blocking API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
users: [],
meta: { resultCount: 0, nextToken: null },
},
error: data.errors?.[0]?.detail ?? 'No blocked users found or invalid response',
}
}
return {
success: true,
output: {
users: data.data.map(transformUser),
meta: {
resultCount: data.meta?.result_count ?? data.data.length,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of blocked user profiles',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+183
View File
@@ -0,0 +1,183 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetBookmarksParams, XTweetListResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetBookmarksTool')
export const xGetBookmarksTool: ToolConfig<XGetBookmarksParams, XTweetListResponse> = {
id: 'x_get_bookmarks',
name: 'X Get Bookmarks',
description: 'Get bookmarked tweets for the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-100)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/users/${params.userId.trim()}/bookmarks?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Bookmarks API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No bookmarks found or invalid response',
output: {
tweets: [],
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
previousToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
previousToken: data.meta?.previous_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of bookmarked tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles',
optional: true,
properties: {
users: {
type: 'array',
description: 'Array of user objects referenced in tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
newestId: { type: 'string', description: 'ID of the newest tweet', optional: true },
oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
previousToken: { type: 'string', description: 'Token for previous page', optional: true },
},
},
},
}
+128
View File
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetFollowersParams, XUserListResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XGetFollowersTool')
export const xGetFollowersTool: ToolConfig<XGetFollowersParams, XUserListResponse> = {
id: 'x_get_followers',
name: 'X Get Followers',
description: 'Get the list of followers for a user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID whose followers to retrieve',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-1000, default 100)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'user.fields': 'created_at,description,profile_image_url,verified,public_metrics,location',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(1000, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/users/${params.userId.trim()}/followers?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Followers API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No followers found or invalid response',
output: {
users: [],
meta: { resultCount: 0, nextToken: null },
},
}
}
return {
success: true,
output: {
users: data.data.map(transformUser),
meta: {
resultCount: data.meta?.result_count ?? data.data.length,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of follower user profiles',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+128
View File
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetFollowingParams, XUserListResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XGetFollowingTool')
export const xGetFollowingTool: ToolConfig<XGetFollowingParams, XUserListResponse> = {
id: 'x_get_following',
name: 'X Get Following',
description: 'Get the list of users that a user is following',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID whose following list to retrieve',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-1000, default 100)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'user.fields': 'created_at,description,profile_image_url,verified,public_metrics,location',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(1000, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/users/${params.userId.trim()}/following?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Following API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No following data found or invalid response',
output: {
users: [],
meta: { resultCount: 0, nextToken: null },
},
}
}
return {
success: true,
output: {
users: data.data.map(transformUser),
meta: {
resultCount: data.meta?.result_count ?? data.data.length,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of users being followed',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+131
View File
@@ -0,0 +1,131 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetLikedTweetsParams, XTweetListResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetLikedTweetsTool')
export const xGetLikedTweetsTool: ToolConfig<XGetLikedTweetsParams, XTweetListResponse> = {
id: 'x_get_liked_tweets',
name: 'X Get Liked Tweets',
description: 'Get tweets liked by a specific user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID whose liked tweets to retrieve',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (5-100)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
expansions: 'author_id,attachments.media_keys',
'tweet.fields': 'created_at,conversation_id,public_metrics,context_annotations',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(5, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/users/${params.userId.trim()}/liked_tweets?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Liked Tweets API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
tweets: [],
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
previousToken: null,
},
},
error: data.errors?.[0]?.detail ?? 'No liked tweets found or invalid response',
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
previousToken: data.meta?.previous_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of liked tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet content' },
createdAt: { type: 'string', description: 'Creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+128
View File
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetLikingUsersParams, XUserListResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XGetLikingUsersTool')
export const xGetLikingUsersTool: ToolConfig<XGetLikingUsersParams, XUserListResponse> = {
id: 'x_get_liking_users',
name: 'X Get Liking Users',
description: 'Get the list of users who liked a specific tweet',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to get liking users for',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-100, default 100)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'user.fields': 'created_at,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/tweets/${params.tweetId.trim()}/liking_users?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Liking Users API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
users: [],
meta: { resultCount: 0, nextToken: null },
},
error: data.errors?.[0]?.detail ?? 'No liking users found or invalid response',
}
}
return {
success: true,
output: {
users: data.data.map(transformUser),
meta: {
resultCount: data.meta?.result_count ?? data.data.length,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of users who liked the tweet',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+88
View File
@@ -0,0 +1,88 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetMeParams, XGetMeResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XGetMeTool')
export const xGetMeTool: ToolConfig<XGetMeParams, XGetMeResponse> = {
id: 'x_get_me',
name: 'X Get Me',
description: "Get the authenticated user's profile information",
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
},
request: {
url: () => {
const queryParams = new URLSearchParams({
'user.fields':
'created_at,description,profile_image_url,verified,public_metrics,location,url',
})
return `https://api.x.com/2/users/me?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Get Me API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to get authenticated user info',
output: {
user: {} as XGetMeResponse['output']['user'],
},
}
}
return {
success: true,
output: {
user: transformUser(data.data),
},
}
},
outputs: {
user: {
type: 'object',
description: 'Authenticated user profile',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
}
@@ -0,0 +1,89 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetPersonalizedTrendsParams, XPersonalizedTrendListResponse } from '@/tools/x/types'
import { transformPersonalizedTrend } from '@/tools/x/types'
const logger = createLogger('XGetPersonalizedTrendsTool')
export const xGetPersonalizedTrendsTool: ToolConfig<
XGetPersonalizedTrendsParams,
XPersonalizedTrendListResponse
> = {
id: 'x_get_personalized_trends',
name: 'X Get Personalized Trends',
description: 'Get personalized trending topics for the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
},
request: {
url: 'https://api.x.com/2/users/personalized_trends?personalized_trend.fields=category,post_count,trend_name,trending_since',
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Personalized Trends API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No personalized trends found or invalid response',
output: {
trends: [],
},
}
}
return {
success: true,
output: {
trends: data.data.map(transformPersonalizedTrend),
},
}
},
outputs: {
trends: {
type: 'array',
description: 'Array of personalized trending topics',
items: {
type: 'object',
properties: {
trendName: { type: 'string', description: 'Name of the trending topic' },
postCount: {
type: 'number',
description: 'Number of posts for this trend',
optional: true,
},
category: {
type: 'string',
description: 'Category of the trend',
optional: true,
},
trendingSince: {
type: 'string',
description: 'ISO 8601 timestamp of when the topic started trending',
optional: true,
},
},
},
},
},
}
+152
View File
@@ -0,0 +1,152 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetQuoteTweetsParams, XTweetListResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetQuoteTweetsTool')
export const xGetQuoteTweetsTool: ToolConfig<XGetQuoteTweetsParams, XTweetListResponse> = {
id: 'x_get_quote_tweets',
name: 'X Get Quote Tweets',
description: 'Get tweets that quote a specific tweet',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to get quote tweets for',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (10-100, default 10)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
expansions: 'author_id,attachments.media_keys',
'tweet.fields': 'created_at,conversation_id,public_metrics,context_annotations',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(10, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/tweets/${params.tweetId.trim()}/quote_tweets?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Quote Tweets API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No quote tweets found or invalid response',
output: {
tweets: [],
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
previousToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
previousToken: data.meta?.previous_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of quote tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: {
type: 'string',
description: 'Conversation thread ID',
optional: true,
},
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+128
View File
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetRetweetedByParams, XUserListResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XGetRetweetedByTool')
export const xGetRetweetedByTool: ToolConfig<XGetRetweetedByParams, XUserListResponse> = {
id: 'x_get_retweeted_by',
name: 'X Get Retweeted By',
description: 'Get the list of users who retweeted a specific tweet',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to get retweeters for',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-100, default 100)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'user.fields': 'created_at,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
return `https://api.x.com/2/tweets/${params.tweetId.trim()}/retweeted_by?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Retweeted By API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No retweeters found or invalid response',
output: {
users: [],
meta: { resultCount: 0, nextToken: null },
},
}
}
return {
success: true,
output: {
users: data.data.map(transformUser),
meta: {
resultCount: data.meta?.result_count ?? data.data.length,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of users who retweeted the tweet',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
},
},
},
}
+100
View File
@@ -0,0 +1,100 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetTrendsByWoeidParams, XTrendListResponse } from '@/tools/x/types'
import { transformTrend } from '@/tools/x/types'
const logger = createLogger('XGetTrendsByWoeidTool')
export const xGetTrendsByWoeidTool: ToolConfig<XGetTrendsByWoeidParams, XTrendListResponse> = {
id: 'x_get_trends_by_woeid',
name: 'X Get Trends By WOEID',
description:
'Get trending topics for a specific location by WOEID (e.g., 1 for worldwide, 23424977 for US)',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
woeid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Yahoo Where On Earth ID (e.g., "1" for worldwide, "23424977" for US, "23424975" for UK)',
},
maxTrends: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of trends to return (1-50, default 20)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'trend.fields': 'trend_name,tweet_count',
})
if (params.maxTrends) {
queryParams.append('max_trends', Number(params.maxTrends).toString())
}
return `https://api.x.com/2/trends/by/woeid/${params.woeid.trim()}?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Trends API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No trends found or invalid response',
output: {
trends: [],
},
}
}
return {
success: true,
output: {
trends: data.data.map(transformTrend),
},
}
},
outputs: {
trends: {
type: 'array',
description: 'Array of trending topics',
items: {
type: 'object',
properties: {
trendName: { type: 'string', description: 'Name of the trending topic' },
tweetCount: {
type: 'number',
description: 'Number of tweets for this trend',
optional: true,
},
},
},
},
},
}
+141
View File
@@ -0,0 +1,141 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetTweetsByIdsParams, XGetTweetsByIdsResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetTweetsByIdsTool')
export const xGetTweetsByIdsTool: ToolConfig<XGetTweetsByIdsParams, XGetTweetsByIdsResponse> = {
id: 'x_get_tweets_by_ids',
name: 'X Get Tweets By IDs',
description: 'Look up multiple tweets by their IDs (up to 100)',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
ids: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated tweet IDs (up to 100)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
ids: params.ids.trim(),
expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
return `https://api.x.com/2/tweets?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get Tweets By IDs API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No tweets found or invalid response',
output: {
tweets: [],
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of tweets matching the provided IDs',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles',
optional: true,
properties: {
users: {
type: 'array',
description: 'Array of user objects referenced in tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
},
},
},
}
+151
View File
@@ -0,0 +1,151 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetUsageParams, XGetUsageResponse } from '@/tools/x/types'
const logger = createLogger('XGetUsageTool')
export const xGetUsageTool: ToolConfig<XGetUsageParams, XGetUsageResponse> = {
id: 'x_get_usage',
name: 'X Get Usage',
description: 'Get the API usage data for your X project',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
days: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of days of usage data to return (1-90, default 7)',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
'usage.fields':
'cap_reset_day,daily_client_app_usage,daily_project_usage,project_cap,project_id,project_usage',
})
if (params.days) {
queryParams.append('days', Number(params.days).toString())
}
return `https://api.x.com/2/usage/tweets?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Get Usage API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to get usage data',
output: {
capResetDay: null,
projectId: '',
projectCap: null,
projectUsage: null,
dailyProjectUsage: [],
dailyClientAppUsage: [],
},
}
}
return {
success: true,
output: {
capResetDay: data.data.cap_reset_day ?? null,
projectId: String(data.data.project_id ?? ''),
projectCap: data.data.project_cap ?? null,
projectUsage: data.data.project_usage ?? null,
dailyProjectUsage: (data.data.daily_project_usage?.usage ?? []).map(
(u: { date: string; usage: number }) => ({
date: u.date,
usage: u.usage ?? 0,
})
),
dailyClientAppUsage: (data.data.daily_client_app_usage ?? []).map(
(app: { client_app_id: string; usage: { date: string; usage: number }[] }) => ({
clientAppId: String(app.client_app_id ?? ''),
usage: (app.usage ?? []).map((u: { date: string; usage: number }) => ({
date: u.date,
usage: u.usage ?? 0,
})),
})
),
},
}
},
outputs: {
capResetDay: {
type: 'number',
description: 'Day of month when usage cap resets',
optional: true,
},
projectId: {
type: 'string',
description: 'The project ID',
},
projectCap: {
type: 'number',
description: 'The project tweet consumption cap',
optional: true,
},
projectUsage: {
type: 'number',
description: 'Total tweets consumed in current period',
optional: true,
},
dailyProjectUsage: {
type: 'array',
description: 'Daily project usage breakdown',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Usage date in ISO 8601 format' },
usage: { type: 'number', description: 'Number of tweets consumed' },
},
},
},
dailyClientAppUsage: {
type: 'array',
description: 'Daily per-app usage breakdown',
items: {
type: 'object',
properties: {
clientAppId: { type: 'string', description: 'Client application ID' },
usage: {
type: 'array',
description: 'Daily usage entries for this app',
items: {
type: 'object',
properties: {
date: { type: 'string', description: 'Usage date in ISO 8601 format' },
usage: { type: 'number', description: 'Number of tweets consumed' },
},
},
},
},
},
},
},
}
+211
View File
@@ -0,0 +1,211 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetUserMentionsParams, XTweetListResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetUserMentionsTool')
export const xGetUserMentionsTool: ToolConfig<XGetUserMentionsParams, XTweetListResponse> = {
id: 'x_get_user_mentions',
name: 'X Get User Mentions',
description: 'Get tweets that mention a specific user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID whose mentions to retrieve',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (5-100, default 10)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Oldest UTC timestamp in ISO 8601 format',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Newest UTC timestamp in ISO 8601 format',
},
sinceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID greater than this',
},
untilId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID less than this',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(5, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
if (params.startTime) queryParams.append('start_time', params.startTime)
if (params.endTime) queryParams.append('end_time', params.endTime)
if (params.sinceId) queryParams.append('since_id', params.sinceId)
if (params.untilId) queryParams.append('until_id', params.untilId)
return `https://api.x.com/2/users/${params.userId.trim()}/mentions?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get User Mentions API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No mentions found or invalid response',
output: {
tweets: [],
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
previousToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
previousToken: data.meta?.previous_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of tweets mentioning the user',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles',
optional: true,
properties: {
users: {
type: 'array',
description: 'Array of user objects referenced in tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
newestId: { type: 'string', description: 'ID of the newest tweet', optional: true },
oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
previousToken: { type: 'string', description: 'Token for previous page', optional: true },
},
},
},
}
+218
View File
@@ -0,0 +1,218 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetUserTimelineParams, XTweetListResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetUserTimelineTool')
export const xGetUserTimelineTool: ToolConfig<XGetUserTimelineParams, XTweetListResponse> = {
id: 'x_get_user_timeline',
name: 'X Get User Timeline',
description: 'Get the reverse chronological home timeline for the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-100, default 10)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Oldest UTC timestamp in ISO 8601 format',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Newest UTC timestamp in ISO 8601 format',
},
sinceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID greater than this',
},
untilId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID less than this',
},
exclude: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated types to exclude: "retweets", "replies"',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
if (params.startTime) queryParams.append('start_time', params.startTime)
if (params.endTime) queryParams.append('end_time', params.endTime)
if (params.sinceId) queryParams.append('since_id', params.sinceId)
if (params.untilId) queryParams.append('until_id', params.untilId)
if (params.exclude) queryParams.append('exclude', params.exclude)
return `https://api.x.com/2/users/${params.userId.trim()}/timelines/reverse_chronological?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get User Timeline API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No timeline data found or invalid response',
output: {
tweets: [],
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
previousToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
previousToken: data.meta?.previous_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of timeline tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles',
optional: true,
properties: {
users: {
type: 'array',
description: 'Array of user objects referenced in tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
newestId: { type: 'string', description: 'ID of the newest tweet', optional: true },
oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
previousToken: { type: 'string', description: 'Token for previous page', optional: true },
},
},
},
}
+218
View File
@@ -0,0 +1,218 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XGetUserTweetsParams, XTweetListResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XGetUserTweetsTool')
export const xGetUserTweetsTool: ToolConfig<XGetUserTweetsParams, XTweetListResponse> = {
id: 'x_get_user_tweets',
name: 'X Get User Tweets',
description: 'Get tweets authored by a specific user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID whose tweets to retrieve',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (5-100, default 10)',
},
paginationToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Oldest UTC timestamp in ISO 8601 format',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Newest UTC timestamp in ISO 8601 format',
},
sinceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID greater than this',
},
untilId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID less than this',
},
exclude: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated types to exclude: "retweets", "replies"',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(5, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.paginationToken) queryParams.append('pagination_token', params.paginationToken)
if (params.startTime) queryParams.append('start_time', params.startTime)
if (params.endTime) queryParams.append('end_time', params.endTime)
if (params.sinceId) queryParams.append('since_id', params.sinceId)
if (params.untilId) queryParams.append('until_id', params.untilId)
if (params.exclude) queryParams.append('exclude', params.exclude)
return `https://api.x.com/2/users/${params.userId.trim()}/tweets?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Get User Tweets API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No tweets found or invalid response',
output: {
tweets: [],
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
previousToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
previousToken: data.meta?.previous_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of tweets by the user',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles',
optional: true,
properties: {
users: {
type: 'array',
description: 'Array of user objects referenced in tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
},
},
meta: {
type: 'object',
description: 'Pagination metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
newestId: { type: 'string', description: 'ID of the newest tweet', optional: true },
oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true },
nextToken: { type: 'string', description: 'Token for next page', optional: true },
previousToken: { type: 'string', description: 'Token for previous page', optional: true },
},
},
},
}
+79
View File
@@ -0,0 +1,79 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XHideReplyParams, XHideReplyResponse } from '@/tools/x/types'
const logger = createLogger('XHideReplyTool')
export const xHideReplyTool: ToolConfig<XHideReplyParams, XHideReplyResponse> = {
id: 'x_hide_reply',
name: 'X Hide Reply',
description: 'Hide or unhide a reply to a tweet authored by the authenticated user',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The reply tweet ID to hide or unhide',
},
hidden: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Set to true to hide the reply, false to unhide',
},
},
request: {
url: (params) => `https://api.x.com/2/tweets/${params.tweetId.trim()}/hidden`,
method: 'PUT',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
hidden: params.hidden,
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Hide Reply API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to hide/unhide reply',
output: {
hidden: false,
},
}
}
return {
success: true,
output: {
hidden: data.data?.hidden ?? false,
},
}
},
outputs: {
hidden: {
type: 'boolean',
description: 'Whether the reply is now hidden',
},
},
}
+39
View File
@@ -0,0 +1,39 @@
import { xReadTool } from '@/tools/x/read'
import { xSearchTool } from '@/tools/x/search'
import { xUserTool } from '@/tools/x/user'
import { xWriteTool } from '@/tools/x/write'
export { xReadTool }
export { xWriteTool }
export { xSearchTool }
export { xUserTool }
export { xCreateBookmarkTool } from '@/tools/x/create_bookmark'
export { xCreateTweetTool } from '@/tools/x/create_tweet'
export { xDeleteBookmarkTool } from '@/tools/x/delete_bookmark'
export { xDeleteTweetTool } from '@/tools/x/delete_tweet'
export { xGetBlockingTool } from '@/tools/x/get_blocking'
export { xGetBookmarksTool } from '@/tools/x/get_bookmarks'
export { xGetFollowersTool } from '@/tools/x/get_followers'
export { xGetFollowingTool } from '@/tools/x/get_following'
export { xGetLikedTweetsTool } from '@/tools/x/get_liked_tweets'
export { xGetLikingUsersTool } from '@/tools/x/get_liking_users'
export { xGetMeTool } from '@/tools/x/get_me'
export { xGetPersonalizedTrendsTool } from '@/tools/x/get_personalized_trends'
export { xGetQuoteTweetsTool } from '@/tools/x/get_quote_tweets'
export { xGetRetweetedByTool } from '@/tools/x/get_retweeted_by'
export { xGetTrendsByWoeidTool } from '@/tools/x/get_trends_by_woeid'
export { xGetTweetsByIdsTool } from '@/tools/x/get_tweets_by_ids'
export { xGetUsageTool } from '@/tools/x/get_usage'
export { xGetUserMentionsTool } from '@/tools/x/get_user_mentions'
export { xGetUserTimelineTool } from '@/tools/x/get_user_timeline'
export { xGetUserTweetsTool } from '@/tools/x/get_user_tweets'
export { xHideReplyTool } from '@/tools/x/hide_reply'
export { xManageBlockTool } from '@/tools/x/manage_block'
export { xManageFollowTool } from '@/tools/x/manage_follow'
export { xManageLikeTool } from '@/tools/x/manage_like'
export { xManageMuteTool } from '@/tools/x/manage_mute'
export { xManageRetweetTool } from '@/tools/x/manage_retweet'
export { xSearchTweetsTool } from '@/tools/x/search_tweets'
export { xSearchUsersTool } from '@/tools/x/search_users'
export * from '@/tools/x/types'
+93
View File
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XManageBlockParams, XManageBlockResponse } from '@/tools/x/types'
const logger = createLogger('XManageBlockTool')
export const xManageBlockTool: ToolConfig<XManageBlockParams, XManageBlockResponse> = {
id: 'x_manage_block',
name: 'X Manage Block',
description: 'Block or unblock a user on X',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
targetUserId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID to block or unblock',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "block" or "unblock"',
},
},
request: {
url: (params) => {
if (params.action === 'unblock') {
return `https://api.x.com/2/users/${params.userId.trim()}/blocking/${params.targetUserId.trim()}`
}
return `https://api.x.com/2/users/${params.userId.trim()}/blocking`
},
method: (params) => (params.action === 'unblock' ? 'DELETE' : 'POST'),
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
if (params.action === 'unblock') return undefined
return {
target_user_id: params.targetUserId.trim(),
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Manage Block API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
blocking: false,
},
error: data.errors?.[0]?.detail ?? 'Failed to manage block',
}
}
return {
success: true,
output: {
blocking: data.data?.blocking ?? false,
},
}
},
outputs: {
blocking: {
type: 'boolean',
description: 'Whether you are now blocking the user',
},
},
}
+99
View File
@@ -0,0 +1,99 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XManageFollowParams, XManageFollowResponse } from '@/tools/x/types'
const logger = createLogger('XManageFollowTool')
export const xManageFollowTool: ToolConfig<XManageFollowParams, XManageFollowResponse> = {
id: 'x_manage_follow',
name: 'X Manage Follow',
description: 'Follow or unfollow a user on X',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
targetUserId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID to follow or unfollow',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "follow" or "unfollow"',
},
},
request: {
url: (params) => {
if (params.action === 'unfollow') {
return `https://api.x.com/2/users/${params.userId.trim()}/following/${params.targetUserId.trim()}`
}
return `https://api.x.com/2/users/${params.userId.trim()}/following`
},
method: (params) => (params.action === 'unfollow' ? 'DELETE' : 'POST'),
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
if (params.action === 'unfollow') return undefined
return {
target_user_id: params.targetUserId.trim(),
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Manage Follow API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
following: false,
pendingFollow: false,
},
error: data.errors?.[0]?.detail ?? 'Failed to manage follow',
}
}
return {
success: true,
output: {
following: data.data?.following ?? false,
pendingFollow: data.data?.pending_follow ?? false,
},
}
},
outputs: {
following: {
type: 'boolean',
description: 'Whether you are now following the user',
},
pendingFollow: {
type: 'boolean',
description: 'Whether the follow request is pending (for protected accounts)',
},
},
}
+93
View File
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XManageLikeParams, XManageLikeResponse } from '@/tools/x/types'
const logger = createLogger('XManageLikeTool')
export const xManageLikeTool: ToolConfig<XManageLikeParams, XManageLikeResponse> = {
id: 'x_manage_like',
name: 'X Manage Like',
description: 'Like or unlike a tweet on X',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to like or unlike',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "like" or "unlike"',
},
},
request: {
url: (params) => {
if (params.action === 'unlike') {
return `https://api.x.com/2/users/${params.userId.trim()}/likes/${params.tweetId.trim()}`
}
return `https://api.x.com/2/users/${params.userId.trim()}/likes`
},
method: (params) => (params.action === 'unlike' ? 'DELETE' : 'POST'),
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
if (params.action === 'unlike') return undefined
return {
tweet_id: params.tweetId.trim(),
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Manage Like API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
liked: false,
},
error: data.errors?.[0]?.detail ?? 'Failed to manage like',
}
}
return {
success: true,
output: {
liked: data.data?.liked ?? false,
},
}
},
outputs: {
liked: {
type: 'boolean',
description: 'Whether the tweet is now liked',
},
},
}
+93
View File
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XManageMuteParams, XManageMuteResponse } from '@/tools/x/types'
const logger = createLogger('XManageMuteTool')
export const xManageMuteTool: ToolConfig<XManageMuteParams, XManageMuteResponse> = {
id: 'x_manage_mute',
name: 'X Manage Mute',
description: 'Mute or unmute a user on X',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
targetUserId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The user ID to mute or unmute',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "mute" or "unmute"',
},
},
request: {
url: (params) => {
if (params.action === 'unmute') {
return `https://api.x.com/2/users/${params.userId.trim()}/muting/${params.targetUserId.trim()}`
}
return `https://api.x.com/2/users/${params.userId.trim()}/muting`
},
method: (params) => (params.action === 'unmute' ? 'DELETE' : 'POST'),
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
if (params.action === 'unmute') return undefined
return {
target_user_id: params.targetUserId.trim(),
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Manage Mute API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'Failed to mute/unmute user',
output: {
muting: false,
},
}
}
return {
success: true,
output: {
muting: data.data?.muting ?? false,
},
}
},
outputs: {
muting: {
type: 'boolean',
description: 'Whether you are now muting the user',
},
},
}
+93
View File
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XManageRetweetParams, XManageRetweetResponse } from '@/tools/x/types'
const logger = createLogger('XManageRetweetTool')
export const xManageRetweetTool: ToolConfig<XManageRetweetParams, XManageRetweetResponse> = {
id: 'x_manage_retweet',
name: 'X Manage Retweet',
description: 'Retweet or unretweet a tweet on X',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The authenticated user ID',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The tweet ID to retweet or unretweet',
},
action: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Action to perform: "retweet" or "unretweet"',
},
},
request: {
url: (params) => {
if (params.action === 'unretweet') {
return `https://api.x.com/2/users/${params.userId.trim()}/retweets/${params.tweetId.trim()}`
}
return `https://api.x.com/2/users/${params.userId.trim()}/retweets`
},
method: (params) => (params.action === 'unretweet' ? 'DELETE' : 'POST'),
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
if (params.action === 'unretweet') return undefined
return {
tweet_id: params.tweetId.trim(),
}
},
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data) {
logger.error('X Manage Retweet API Error:', JSON.stringify(data, null, 2))
return {
success: false,
output: {
retweeted: false,
},
error: data.errors?.[0]?.detail ?? 'Failed to manage retweet',
}
}
return {
success: true,
output: {
retweeted: data.data?.retweeted ?? false,
},
}
},
outputs: {
retweeted: {
type: 'boolean',
description: 'Whether the tweet is now retweeted',
},
},
}
+187
View File
@@ -0,0 +1,187 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XReadParams, XReadResponse, XTweet } from '@/tools/x/types'
import { transformTweet } from '@/tools/x/types'
const logger = createLogger('XReadTool')
export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
id: 'x_read',
name: 'X Read',
description: 'Read tweet details, including replies and conversation context',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
tweetId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the tweet to read (e.g., 1234567890123456789)',
},
includeReplies: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether to include replies to the tweet',
},
},
request: {
url: (params) => {
const expansions = [
'author_id',
'in_reply_to_user_id',
'referenced_tweets.id',
'referenced_tweets.id.author_id',
'attachments.media_keys',
'attachments.poll_ids',
].join(',')
const tweetFields = [
'created_at',
'conversation_id',
'in_reply_to_user_id',
'attachments',
'context_annotations',
'public_metrics',
].join(',')
const userFields = [
'name',
'username',
'description',
'profile_image_url',
'verified',
'public_metrics',
].join(',')
const queryParams = new URLSearchParams({
expansions,
'tweet.fields': tweetFields,
'user.fields': userFields,
})
return `https://api.twitter.com/2/tweets/${params.tweetId}?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params) => {
const data = await response.json()
if (data.errors && !data.data) {
logger.error('X Read API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || data.errors?.[0]?.message || 'Failed to fetch tweet',
output: {
tweet: {} as XTweet,
},
}
}
const mainTweet = transformTweet(data.data)
const context: { parentTweet?: XTweet; rootTweet?: XTweet } = {}
if (data.includes?.tweets) {
const referencedTweets = data.data.referenced_tweets || []
const parentTweetRef = referencedTweets.find((ref: any) => ref.type === 'replied_to')
const quotedTweetRef = referencedTweets.find((ref: any) => ref.type === 'quoted')
if (parentTweetRef) {
const parentTweet = data.includes.tweets.find((t: any) => t.id === parentTweetRef.id)
if (parentTweet) context.parentTweet = transformTweet(parentTweet)
}
if (!parentTweetRef && quotedTweetRef) {
const quotedTweet = data.includes.tweets.find((t: any) => t.id === quotedTweetRef.id)
if (quotedTweet) context.rootTweet = transformTweet(quotedTweet)
}
}
let replies: XTweet[] = []
if (params?.includeReplies && mainTweet.id) {
try {
const repliesExpansions = ['author_id', 'referenced_tweets.id'].join(',')
const repliesTweetFields = [
'created_at',
'conversation_id',
'in_reply_to_user_id',
'public_metrics',
].join(',')
const conversationId = mainTweet.conversationId || mainTweet.id
const searchQuery = `conversation_id:${conversationId}`
const searchParams = new URLSearchParams({
query: searchQuery,
expansions: repliesExpansions,
'tweet.fields': repliesTweetFields,
max_results: '100', // Max allowed
})
const repliesResponse = await fetch(
`https://api.twitter.com/2/tweets/search/recent?${searchParams.toString()}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${params?.accessToken || ''}`,
'Content-Type': 'application/json',
},
}
)
const repliesData = await repliesResponse.json()
if (repliesData.data && Array.isArray(repliesData.data)) {
replies = repliesData.data
.filter((tweet: any) => tweet.id !== mainTweet.id)
.map(transformTweet)
}
} catch (error) {
logger.warn('Failed to fetch replies:', error)
}
}
return {
success: true,
output: {
tweet: mainTweet,
replies: replies.length > 0 ? replies : undefined,
context: Object.keys(context).length > 0 ? context : undefined,
},
}
},
outputs: {
tweet: {
type: 'object',
description: 'The main tweet data',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet content text' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'ID of the tweet author' },
},
},
context: {
type: 'object',
description: 'Conversation context including parent and root tweets',
optional: true,
},
},
}
+171
View File
@@ -0,0 +1,171 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XSearchParams, XSearchResponse } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XSearchTool')
export const xSearchTool: ToolConfig<XSearchParams, XSearchResponse> = {
id: 'x_search',
name: 'X Search',
description: 'Search for tweets using keywords, hashtags, or advanced queries',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Search query (e.g., "AI news", "#technology", "from:username"). Supports X search operators',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results to return (e.g., 10, 25, 50). Default: 10, max: 100',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Start time for search (ISO 8601 format)',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'End time for search (ISO 8601 format)',
},
sortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order for results (recency or relevancy)',
},
},
request: {
url: (params) => {
const query = params.query
const expansions = [
'author_id',
'referenced_tweets.id',
'attachments.media_keys',
'attachments.poll_ids',
].join(',')
const queryParams = new URLSearchParams({
query,
expansions,
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults && Number(params.maxResults) < 10) {
queryParams.append('max_results', '10')
} else if (params.maxResults) {
queryParams.append('max_results', Number(params.maxResults).toString())
}
if (params.startTime) queryParams.append('start_time', params.startTime)
if (params.endTime) queryParams.append('end_time', params.endTime)
if (params.sortOrder) queryParams.append('sort_order', params.sortOrder)
return `https://api.twitter.com/2/tweets/search/recent?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Search API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error:
data.error?.detail ||
data.error?.title ||
'No results found or invalid response from X API',
output: {
tweets: [],
includes: {
users: [],
media: [],
polls: [],
},
meta: data.meta || {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) || [],
media: data.includes?.media || [],
polls: data.includes?.polls || [],
},
meta: {
resultCount: data.meta.result_count,
newestId: data.meta.newest_id,
oldestId: data.meta.oldest_id,
nextToken: data.meta.next_token,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of tweets matching the search query',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet content' },
createdAt: { type: 'string', description: 'Creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles and media',
optional: true,
},
meta: {
type: 'object',
description: 'Search metadata including result count and pagination tokens',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
newestId: { type: 'string', description: 'ID of the newest tweet' },
oldestId: { type: 'string', description: 'ID of the oldest tweet' },
},
},
},
}
+239
View File
@@ -0,0 +1,239 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XSearchTweetsParams, XTweet, XUser } from '@/tools/x/types'
import { transformTweet, transformUser } from '@/tools/x/types'
const logger = createLogger('XSearchTweetsTool')
interface XSearchTweetsResponse {
success: boolean
output: {
tweets: XTweet[]
includes?: { users: XUser[] }
meta: {
resultCount: number
newestId: string | null
oldestId: string | null
nextToken: string | null
}
}
}
export const xSearchTweetsTool: ToolConfig<XSearchTweetsParams, XSearchTweetsResponse> = {
id: 'x_search_tweets',
name: 'X Search Tweets',
description: 'Search for recent tweets using keywords, hashtags, or advanced query operators',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Search query (supports operators like "from:", "to:", "#hashtag", "has:images", "is:retweet", "lang:")',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (10-100, default 10)',
},
startTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Oldest UTC timestamp in ISO 8601 format (e.g., 2024-01-01T00:00:00Z)',
},
endTime: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Newest UTC timestamp in ISO 8601 format',
},
sinceId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID greater than this',
},
untilId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Returns tweets with ID less than this',
},
sortOrder: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort order: "recency" or "relevancy"',
},
nextToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page of results',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
query: params.query,
expansions: 'author_id,referenced_tweets.id,attachments.media_keys,attachments.poll_ids',
'tweet.fields':
'created_at,conversation_id,in_reply_to_user_id,attachments,context_annotations,public_metrics',
'user.fields': 'name,username,description,profile_image_url,verified,public_metrics',
})
if (params.maxResults) {
const max = Math.max(10, Math.min(100, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.startTime) queryParams.append('start_time', params.startTime)
if (params.endTime) queryParams.append('end_time', params.endTime)
if (params.sinceId) queryParams.append('since_id', params.sinceId)
if (params.untilId) queryParams.append('until_id', params.untilId)
if (params.sortOrder) queryParams.append('sort_order', params.sortOrder)
if (params.nextToken) queryParams.append('next_token', params.nextToken)
return `https://api.x.com/2/tweets/search/recent?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Search Tweets API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error:
data.errors?.[0]?.detail ||
data.errors?.[0]?.title ||
'No results found or invalid response from X API',
output: {
tweets: [],
includes: { users: [] },
meta: {
resultCount: 0,
newestId: null,
oldestId: null,
nextToken: null,
},
},
}
}
return {
success: true,
output: {
tweets: data.data.map(transformTweet),
includes: {
users: data.includes?.users?.map(transformUser) ?? [],
},
meta: {
resultCount: data.meta?.result_count ?? 0,
newestId: data.meta?.newest_id ?? null,
oldestId: data.meta?.oldest_id ?? null,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
tweets: {
type: 'array',
description: 'Array of tweets matching the search query',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet text content' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'Author user ID' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
inReplyToUserId: {
type: 'string',
description: 'User ID being replied to',
optional: true,
},
publicMetrics: {
type: 'object',
description: 'Engagement metrics',
optional: true,
properties: {
retweetCount: { type: 'number', description: 'Number of retweets' },
replyCount: { type: 'number', description: 'Number of replies' },
likeCount: { type: 'number', description: 'Number of likes' },
quoteCount: { type: 'number', description: 'Number of quotes' },
},
},
},
},
},
includes: {
type: 'object',
description: 'Additional data including user profiles',
optional: true,
properties: {
users: {
type: 'array',
description: 'Array of user objects referenced in tweets',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
},
},
meta: {
type: 'object',
description: 'Search metadata including result count and pagination tokens',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
newestId: { type: 'string', description: 'ID of the newest tweet', optional: true },
oldestId: { type: 'string', description: 'ID of the oldest tweet', optional: true },
nextToken: {
type: 'string',
description: 'Pagination token for next page',
optional: true,
},
},
},
},
}
+133
View File
@@ -0,0 +1,133 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XSearchUsersParams, XUserListResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XSearchUsersTool')
export const xSearchUsersTool: ToolConfig<XSearchUsersParams, XUserListResponse> = {
id: 'x_search_users',
name: 'X Search Users',
description: 'Search for X users by name, username, or bio',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
query: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Search keyword (1-50 chars, matches name, username, or bio)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of results (1-1000, default 100)',
},
nextToken: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination token for next page',
},
},
request: {
url: (params) => {
const queryParams = new URLSearchParams({
query: params.query,
'user.fields': 'created_at,description,profile_image_url,verified,public_metrics,location',
})
if (params.maxResults) {
const max = Math.max(1, Math.min(1000, Number(params.maxResults)))
queryParams.append('max_results', max.toString())
}
if (params.nextToken) queryParams.append('next_token', params.nextToken)
return `https://api.x.com/2/users/search?${queryParams.toString()}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response) => {
const data = await response.json()
if (!data.data || !Array.isArray(data.data)) {
logger.error('X Search Users API Error:', JSON.stringify(data, null, 2))
return {
success: false,
error: data.errors?.[0]?.detail || 'No users found or invalid response',
output: {
users: [],
meta: { resultCount: 0, nextToken: null },
},
}
}
return {
success: true,
output: {
users: data.data.map(transformUser),
meta: {
resultCount: data.meta?.result_count ?? data.data.length,
nextToken: data.meta?.next_token ?? null,
},
},
}
},
outputs: {
users: {
type: 'array',
description: 'Array of users matching the search query',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio', optional: true },
profileImageUrl: { type: 'string', description: 'Profile image URL', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
meta: {
type: 'object',
description: 'Search metadata',
properties: {
resultCount: { type: 'number', description: 'Number of results returned' },
nextToken: {
type: 'string',
description: 'Pagination token for next page',
optional: true,
},
},
},
},
}
+522
View File
@@ -0,0 +1,522 @@
import type { ToolResponse } from '@/tools/types'
/**
* Context annotation domain from X API
*/
interface XContextAnnotationDomain {
id: string
name: string
description?: string
}
/**
* Context annotation entity from X API
*/
interface XContextAnnotationEntity {
id: string
name: string
description?: string
}
/**
* Context annotation from X API - provides semantic context about tweet content
*/
interface XContextAnnotation {
domain: XContextAnnotationDomain
entity: XContextAnnotationEntity
}
/**
* Tweet object from X API
*/
export interface XTweet {
id: string
text: string
createdAt: string
authorId: string
conversationId?: string
inReplyToUserId?: string
attachments?: {
mediaKeys?: string[]
pollId?: string
}
contextAnnotations?: XContextAnnotation[]
publicMetrics?: {
retweetCount: number
replyCount: number
likeCount: number
quoteCount: number
}
}
export interface XUser {
id: string
username: string
name: string
description?: string
profileImageUrl?: string
verified: boolean
metrics: {
followersCount: number
followingCount: number
tweetCount: number
}
}
// Common parameters for all X endpoints
interface XBaseParams {
accessToken: string
}
// Write Operation
export interface XWriteParams extends XBaseParams {
text: string
replyTo?: string
mediaIds?: string[]
poll?: {
options: string[]
durationMinutes: number
}
}
export interface XWriteResponse extends ToolResponse {
output: {
tweet: XTweet
}
}
// Read Operation
export interface XReadParams extends XBaseParams {
tweetId: string
includeReplies?: boolean
}
export interface XReadResponse extends ToolResponse {
output: {
tweet: XTweet
replies?: XTweet[]
context?: {
parentTweet?: XTweet
rootTweet?: XTweet
}
}
}
// Search Operation
export interface XSearchParams extends XBaseParams {
query: string
maxResults?: number
startTime?: string
endTime?: string
sortOrder?: 'recency' | 'relevancy'
}
export interface XSearchResponse extends ToolResponse {
output: {
tweets: XTweet[]
includes?: {
users: XUser[]
media: any[]
polls: any[]
}
meta: {
resultCount: number
newestId: string
oldestId: string
nextToken?: string
}
}
}
// User Operation
export interface XUserParams extends XBaseParams {
username: string
includeRecentTweets?: boolean
}
export interface XUserResponse extends ToolResponse {
output: {
user: XUser
recentTweets?: XTweet[]
}
}
export type XResponse = XWriteResponse | XReadResponse | XSearchResponse | XUserResponse
/**
* Transforms raw X API tweet data (snake_case) into the XTweet format (camelCase)
*/
export const transformTweet = (tweet: any): XTweet => ({
id: tweet.id,
text: tweet.text,
createdAt: tweet.created_at,
authorId: tweet.author_id,
conversationId: tweet.conversation_id,
inReplyToUserId: tweet.in_reply_to_user_id,
attachments: {
mediaKeys: tweet.attachments?.media_keys,
pollId: tweet.attachments?.poll_ids?.[0],
},
contextAnnotations: tweet.context_annotations,
publicMetrics: tweet.public_metrics
? {
retweetCount: tweet.public_metrics.retweet_count,
replyCount: tweet.public_metrics.reply_count,
likeCount: tweet.public_metrics.like_count,
quoteCount: tweet.public_metrics.quote_count,
}
: undefined,
})
/**
* Transforms raw X API user data (snake_case) into the XUser format (camelCase)
*/
export const transformUser = (user: any): XUser => ({
id: user.id,
username: user.username,
name: user.name || '',
description: user.description || '',
profileImageUrl: user.profile_image_url || '',
verified: !!user.verified,
metrics: {
followersCount: user.public_metrics?.followers_count || 0,
followingCount: user.public_metrics?.following_count || 0,
tweetCount: user.public_metrics?.tweet_count || 0,
},
})
/**
* Trend object from X API (WOEID trends)
*/
export interface XTrend {
trendName: string
tweetCount: number | null
}
/**
* Personalized trend object from X API
*/
export interface XPersonalizedTrend {
trendName: string
postCount: number | null
category: string | null
trendingSince: string | null
}
/**
* Transforms raw X API trend data (WOEID) into the XTrend format
*/
export const transformTrend = (trend: any): XTrend => ({
trendName: trend.trend_name ?? trend.name ?? '',
tweetCount: trend.tweet_count ?? null,
})
/**
* Transforms raw X API personalized trend data into the XPersonalizedTrend format
*/
export const transformPersonalizedTrend = (trend: any): XPersonalizedTrend => ({
trendName: trend.trend_name ?? '',
postCount: trend.post_count ?? null,
category: trend.category ?? null,
trendingSince: trend.trending_since ?? null,
})
// --- New Tool Parameter Interfaces ---
export interface XSearchTweetsParams extends XBaseParams {
query: string
maxResults?: number
startTime?: string
endTime?: string
sinceId?: string
untilId?: string
sortOrder?: string
nextToken?: string
}
export interface XGetUserTweetsParams extends XBaseParams {
userId: string
maxResults?: number
startTime?: string
endTime?: string
sinceId?: string
untilId?: string
exclude?: string
paginationToken?: string
}
export interface XGetUserMentionsParams extends XBaseParams {
userId: string
maxResults?: number
startTime?: string
endTime?: string
sinceId?: string
untilId?: string
paginationToken?: string
}
export interface XGetUserTimelineParams extends XBaseParams {
userId: string
maxResults?: number
startTime?: string
endTime?: string
sinceId?: string
untilId?: string
exclude?: string
paginationToken?: string
}
export interface XGetTweetsByIdsParams extends XBaseParams {
ids: string
}
export interface XGetTweetsByIdsResponse extends ToolResponse {
output: {
tweets: XTweet[]
}
}
export interface XGetBookmarksParams extends XBaseParams {
userId: string
maxResults?: number
paginationToken?: string
}
export interface XCreateBookmarkParams extends XBaseParams {
userId: string
tweetId: string
}
export interface XCreateBookmarkResponse extends ToolResponse {
output: {
bookmarked: boolean
}
}
export interface XDeleteBookmarkParams extends XBaseParams {
userId: string
tweetId: string
}
export interface XDeleteBookmarkResponse extends ToolResponse {
output: {
bookmarked: boolean
}
}
export interface XCreateTweetParams extends XBaseParams {
text: string
replyToTweetId?: string
quoteTweetId?: string
mediaIds?: string
replySettings?: string
}
export interface XCreateTweetResponse extends ToolResponse {
output: {
id: string
text: string
}
}
export interface XDeleteTweetParams extends XBaseParams {
tweetId: string
}
export interface XDeleteTweetResponse extends ToolResponse {
output: {
deleted: boolean
}
}
export interface XGetMeParams extends XBaseParams {}
export interface XGetMeResponse extends ToolResponse {
output: {
user: XUser
}
}
export interface XSearchUsersParams extends XBaseParams {
query: string
maxResults?: number
nextToken?: string
}
export interface XGetFollowersParams extends XBaseParams {
userId: string
maxResults?: number
paginationToken?: string
}
export interface XGetFollowingParams extends XBaseParams {
userId: string
maxResults?: number
paginationToken?: string
}
export interface XManageFollowParams extends XBaseParams {
userId: string
targetUserId: string
action: string
}
export interface XManageFollowResponse extends ToolResponse {
output: {
following: boolean
pendingFollow: boolean
}
}
export interface XGetBlockingParams extends XBaseParams {
userId: string
maxResults?: number
paginationToken?: string
}
export interface XManageBlockParams extends XBaseParams {
userId: string
targetUserId: string
action: string
}
export interface XManageBlockResponse extends ToolResponse {
output: {
blocking: boolean
}
}
export interface XGetLikedTweetsParams extends XBaseParams {
userId: string
maxResults?: number
paginationToken?: string
}
export interface XGetLikingUsersParams extends XBaseParams {
tweetId: string
maxResults?: number
paginationToken?: string
}
export interface XManageLikeParams extends XBaseParams {
userId: string
tweetId: string
action: string
}
export interface XManageLikeResponse extends ToolResponse {
output: {
liked: boolean
}
}
export interface XManageRetweetParams extends XBaseParams {
userId: string
tweetId: string
action: string
}
export interface XManageRetweetResponse extends ToolResponse {
output: {
retweeted: boolean
}
}
export interface XGetRetweetedByParams extends XBaseParams {
tweetId: string
maxResults?: number
paginationToken?: string
}
export interface XGetQuoteTweetsParams extends XBaseParams {
tweetId: string
maxResults?: number
paginationToken?: string
}
export interface XGetTrendsByWoeidParams extends XBaseParams {
woeid: string
maxTrends?: number
}
export interface XGetPersonalizedTrendsParams extends XBaseParams {}
export interface XGetUsageParams extends XBaseParams {
days?: number
}
export interface XGetUsageResponse extends ToolResponse {
output: {
capResetDay: number | null
projectId: string
projectCap: number | null
projectUsage: number | null
dailyProjectUsage: Array<{ date: string; usage: number }>
dailyClientAppUsage: Array<{
clientAppId: string
usage: Array<{ date: string; usage: number }>
}>
}
}
export interface XHideReplyParams extends XBaseParams {
tweetId: string
hidden: boolean
}
export interface XHideReplyResponse extends ToolResponse {
output: {
hidden: boolean
}
}
export interface XManageMuteParams extends XBaseParams {
userId: string
targetUserId: string
action: string
}
export interface XManageMuteResponse extends ToolResponse {
output: {
muting: boolean
}
}
// Common response types for list endpoints
export interface XTweetListResponse extends ToolResponse {
output: {
tweets: XTweet[]
includes?: {
users: XUser[]
}
meta: {
resultCount: number
newestId: string | null
oldestId: string | null
nextToken: string | null
previousToken: string | null
}
}
}
export interface XUserListResponse extends ToolResponse {
output: {
users: XUser[]
meta: {
resultCount: number
nextToken: string | null
}
}
}
export interface XTrendListResponse extends ToolResponse {
output: {
trends: XTrend[]
}
}
export interface XPersonalizedTrendListResponse extends ToolResponse {
output: {
trends: XPersonalizedTrend[]
}
}
+128
View File
@@ -0,0 +1,128 @@
import { createLogger } from '@sim/logger'
import type { ToolConfig } from '@/tools/types'
import type { XUserParams, XUserResponse } from '@/tools/x/types'
import { transformUser } from '@/tools/x/types'
const logger = createLogger('XUserTool')
export const xUserTool: ToolConfig<XUserParams, XUserResponse> = {
id: 'x_user',
name: 'X User',
description: 'Get user profile information',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
username: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Username to look up without @ symbol (e.g., elonmusk, openai)',
},
},
request: {
url: (params) => {
const username = encodeURIComponent(params.username)
// Keep fields minimal to reduce chance of rate limits
const userFields = 'description,profile_image_url,verified,public_metrics'
return `https://api.twitter.com/2/users/by/username/${username}?user.fields=${userFields}`
},
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
},
transformResponse: async (response, params) => {
// Handle rate limit issues (429 status code)
if (response.status === 429) {
logger.warn('X API rate limit exceeded', {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
})
// Try to extract rate limit reset time from headers if available
const resetTime = response.headers.get('x-rate-limit-reset')
const message = resetTime
? `Rate limit exceeded. Please try again after ${new Date(Number.parseInt(resetTime) * 1000).toLocaleTimeString()}.`
: 'X API rate limit exceeded. Please try again later.'
throw new Error(message)
}
try {
const responseData = await response.json()
logger.debug('X API response', {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
responseData,
})
// Check if response contains expected data structure
if (!responseData.data) {
// If there's an error object in the response
if (responseData.errors && responseData.errors.length > 0) {
const error = responseData.errors[0]
// Remove the square brackets from the error message
const cleanedMessage = error.detail ? error.detail.replace(/\[(.*?)\]/, '$1') : ''
throw new Error(
`X API error: ${cleanedMessage || error.message || JSON.stringify(error)}`
)
}
throw new Error('Invalid response format from X API')
}
const userData = responseData.data
const user = transformUser(userData)
return {
success: true,
output: {
user,
},
}
} catch (error) {
logger.error('Error processing X API response', {
error,
status: response.status,
})
throw error
}
},
outputs: {
user: {
type: 'object',
description: 'X user profile information',
properties: {
id: { type: 'string', description: 'User ID' },
username: { type: 'string', description: 'Username without @ symbol' },
name: { type: 'string', description: 'Display name' },
description: { type: 'string', description: 'User bio/description', optional: true },
verified: { type: 'boolean', description: 'Whether the user is verified' },
metrics: {
type: 'object',
description: 'User statistics',
properties: {
followersCount: { type: 'number', description: 'Number of followers' },
followingCount: { type: 'number', description: 'Number of users following' },
tweetCount: { type: 'number', description: 'Total number of tweets' },
},
},
},
},
},
}
+112
View File
@@ -0,0 +1,112 @@
import type { ToolConfig } from '@/tools/types'
import type { XWriteParams, XWriteResponse } from '@/tools/x/types'
import { transformTweet } from '@/tools/x/types'
export const xWriteTool: ToolConfig<XWriteParams, XWriteResponse> = {
id: 'x_write',
name: 'X Write',
description: 'Post new tweets, reply to tweets, or create polls on X (Twitter)',
version: '1.0.0',
oauth: {
required: true,
provider: 'x',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'X OAuth access token',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text content of your tweet (max 280 characters)',
},
replyTo: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'ID of the tweet to reply to (e.g., 1234567890123456789)',
},
mediaIds: {
type: 'array',
required: false,
visibility: 'user-only',
description: 'Array of media IDs to attach to the tweet',
},
poll: {
type: 'object',
required: false,
visibility: 'user-only',
description: 'Poll configuration for the tweet',
},
},
request: {
url: 'https://api.twitter.com/2/tweets',
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.accessToken}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: any = {
text: params.text,
}
if (params.replyTo) {
body.reply = { in_reply_to_tweet_id: params.replyTo }
}
if (params.mediaIds?.length) {
body.media = { media_ids: params.mediaIds }
}
if (params.poll) {
body.poll = {
options: params.poll.options,
duration_minutes: params.poll.durationMinutes,
}
}
return body
},
},
transformResponse: async (response) => {
const data = await response.json()
return {
success: true,
output: {
tweet: transformTweet(data.data),
},
}
},
outputs: {
tweet: {
type: 'object',
description: 'The newly created tweet data',
properties: {
id: { type: 'string', description: 'Tweet ID' },
text: { type: 'string', description: 'Tweet content text' },
createdAt: { type: 'string', description: 'Tweet creation timestamp' },
authorId: { type: 'string', description: 'ID of the tweet author' },
conversationId: { type: 'string', description: 'Conversation thread ID', optional: true },
attachments: {
type: 'object',
description: 'Media or poll attachments',
optional: true,
properties: {
mediaKeys: { type: 'array', description: 'Media attachment keys', optional: true },
pollId: { type: 'string', description: 'Poll ID if poll attached', optional: true },
},
},
},
},
},
}