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
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:
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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)',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user