1+ import { createLogger } from '@/lib/logs/console/logger'
12import type { ToolConfig } from '@/tools/types'
23import type { XReadParams , XReadResponse , XTweet } from '@/tools/x/types'
4+ import { transformTweet } from '@/tools/x/types'
5+
6+ const logger = createLogger ( 'XReadTool' )
37
48export const xReadTool : ToolConfig < XReadParams , XReadResponse > = {
59 id : 'x_read' ,
@@ -39,11 +43,36 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
3943 'author_id' ,
4044 'in_reply_to_user_id' ,
4145 'referenced_tweets.id' ,
46+ 'referenced_tweets.id.author_id' ,
4247 'attachments.media_keys' ,
4348 'attachments.poll_ids' ,
4449 ] . join ( ',' )
4550
46- return `https://api.twitter.com/2/tweets/${ params . tweetId } ?expansions=${ expansions } `
51+ const tweetFields = [
52+ 'created_at' ,
53+ 'conversation_id' ,
54+ 'in_reply_to_user_id' ,
55+ 'attachments' ,
56+ 'context_annotations' ,
57+ 'public_metrics' ,
58+ ] . join ( ',' )
59+
60+ const userFields = [
61+ 'name' ,
62+ 'username' ,
63+ 'description' ,
64+ 'profile_image_url' ,
65+ 'verified' ,
66+ 'public_metrics' ,
67+ ] . join ( ',' )
68+
69+ const queryParams = new URLSearchParams ( {
70+ expansions,
71+ 'tweet.fields' : tweetFields ,
72+ 'user.fields' : userFields ,
73+ } )
74+
75+ return `https://api.twitter.com/2/tweets/${ params . tweetId } ?${ queryParams . toString ( ) } `
4776 } ,
4877 method : 'GET' ,
4978 headers : ( params ) => ( {
@@ -52,47 +81,88 @@ export const xReadTool: ToolConfig<XReadParams, XReadResponse> = {
5281 } ) ,
5382 } ,
5483
55- transformResponse : async ( response ) => {
84+ transformResponse : async ( response , params ) => {
5685 const data = await response . json ( )
5786
58- const transformTweet = ( tweet : any ) : XTweet => ( {
59- id : tweet . id ,
60- text : tweet . text ,
61- createdAt : tweet . created_at ,
62- authorId : tweet . author_id ,
63- conversationId : tweet . conversation_id ,
64- inReplyToUserId : tweet . in_reply_to_user_id ,
65- attachments : {
66- mediaKeys : tweet . attachments ?. media_keys ,
67- pollId : tweet . attachments ?. poll_ids ?. [ 0 ] ,
68- } ,
69- } )
87+ if ( data . errors && ! data . data ) {
88+ logger . error ( 'X Read API Error:' , JSON . stringify ( data , null , 2 ) )
89+ return {
90+ success : false ,
91+ error : data . errors ?. [ 0 ] ?. detail || data . errors ?. [ 0 ] ?. message || 'Failed to fetch tweet' ,
92+ output : {
93+ tweet : { } as XTweet ,
94+ } ,
95+ }
96+ }
7097
7198 const mainTweet = transformTweet ( data . data )
7299 const context : { parentTweet ?: XTweet ; rootTweet ?: XTweet } = { }
73100
74- // Get parent and root tweets if available
75101 if ( data . includes ?. tweets ) {
76102 const referencedTweets = data . data . referenced_tweets || [ ]
77103 const parentTweetRef = referencedTweets . find ( ( ref : any ) => ref . type === 'replied_to' )
78- const rootTweetRef = referencedTweets . find ( ( ref : any ) => ref . type === 'replied_to_root ' )
104+ const quotedTweetRef = referencedTweets . find ( ( ref : any ) => ref . type === 'quoted ' )
79105
80106 if ( parentTweetRef ) {
81107 const parentTweet = data . includes . tweets . find ( ( t : any ) => t . id === parentTweetRef . id )
82108 if ( parentTweet ) context . parentTweet = transformTweet ( parentTweet )
83109 }
84110
85- if ( rootTweetRef ) {
86- const rootTweet = data . includes . tweets . find ( ( t : any ) => t . id === rootTweetRef . id )
87- if ( rootTweet ) context . rootTweet = transformTweet ( rootTweet )
111+ if ( ! parentTweetRef && quotedTweetRef ) {
112+ const quotedTweet = data . includes . tweets . find ( ( t : any ) => t . id === quotedTweetRef . id )
113+ if ( quotedTweet ) context . rootTweet = transformTweet ( quotedTweet )
114+ }
115+ }
116+
117+ let replies : XTweet [ ] = [ ]
118+ if ( params ?. includeReplies && mainTweet . id ) {
119+ try {
120+ const repliesExpansions = [ 'author_id' , 'referenced_tweets.id' ] . join ( ',' )
121+ const repliesTweetFields = [
122+ 'created_at' ,
123+ 'conversation_id' ,
124+ 'in_reply_to_user_id' ,
125+ 'public_metrics' ,
126+ ] . join ( ',' )
127+
128+ const conversationId = mainTweet . conversationId || mainTweet . id
129+ const searchQuery = `conversation_id:${ conversationId } `
130+ const searchParams = new URLSearchParams ( {
131+ query : searchQuery ,
132+ expansions : repliesExpansions ,
133+ 'tweet.fields' : repliesTweetFields ,
134+ max_results : '100' , // Max allowed
135+ } )
136+
137+ const repliesResponse = await fetch (
138+ `https://api.twitter.com/2/tweets/search/recent?${ searchParams . toString ( ) } ` ,
139+ {
140+ method : 'GET' ,
141+ headers : {
142+ Authorization : `Bearer ${ params ?. accessToken || '' } ` ,
143+ 'Content-Type' : 'application/json' ,
144+ } ,
145+ }
146+ )
147+
148+ const repliesData = await repliesResponse . json ( )
149+
150+ if ( repliesData . data && Array . isArray ( repliesData . data ) ) {
151+ replies = repliesData . data
152+ . filter ( ( tweet : any ) => tweet . id !== mainTweet . id )
153+ . map ( transformTweet )
154+ }
155+ } catch ( error ) {
156+ logger . warn ( 'Failed to fetch replies:' , error )
88157 }
89158 }
90159
91160 return {
92161 success : true ,
93162 output : {
94163 tweet : mainTweet ,
95- context,
164+ replies : replies . length > 0 ? replies : undefined ,
165+ context : Object . keys ( context ) . length > 0 ? context : undefined ,
96166 } ,
97167 }
98168 } ,
0 commit comments