diff --git a/README.md b/README.md index 23ca054..8b858b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MyXstack -This repository hosts a lightweight, step-by-step guide for setting up an autonomous X (Twitter) agent system that acts based on thread context & reasoning, through Grok via the xMCP server. +This repository hosts a lightweight, step-by-step guide for setting up an autonomous X (Twitter) agent system that acts based on thread context and reasoning, through Grok via the xMCP server. ## Phase 1: Gather prerequisites & accounts (1–2 hours) @@ -20,8 +20,8 @@ This repository hosts a lightweight, step-by-step guide for setting up an autono 3. Note the bot user ID (you can fetch this via the API later). ### xAI / Grok API key -. Visit , open the API keys section, and create a key that starts with `xai-`. -. Store the key securely. +1. Visit , open the API keys section, and create a key that starts with `xai-`. +2. Store the key securely. ### Local tooling - Install Python 3.9+ (3.10–3.13 recommended). diff --git a/package-lock.json b/package-lock.json index 9b3d662..790aab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -395,6 +395,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1158,6 +1159,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/index.ts b/src/index.ts index 6dd2dfe..30e4020 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,10 @@ import { XAPIClient } from './services/xapi.js'; import { AutonomousAgent } from './services/agent.js'; import { XMCPServer } from './mcp/server.js'; +// Redirect console.log to stderr to prevent conflict with MCP StdioServerTransport +// which hijacks stdout for protocol messages +console.log = (...args: unknown[]) => console.error(...args); + async function main() { console.log('═══════════════════════════════════════════════════'); console.log(' MyXstack - Autonomous AI Agent on X (Twitter)'); diff --git a/src/services/agent.ts b/src/services/agent.ts index 326fb38..2d19839 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -14,6 +14,7 @@ export class AutonomousAgent { private isRunning: boolean = false; private pollingIntervalId: NodeJS.Timeout | null = null; private isProcessing: boolean = false; + private static readonly MAX_PROCESSED_MENTIONS = 10_000; constructor(config: AgentConfig, xClient: XAPIClient) { this.config = config; @@ -90,10 +91,20 @@ export class AutonomousAgent { console.log(`\n📬 [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`); - // Process each mention - for (const mention of newMentions) { + // Process mentions oldest-first (API returns newest-first) for chronological Set insertion + for (const mention of [...newMentions].reverse()) { await this.processMention(mention); this.processedMentions.add(mention.post.id); + + // Prune oldest entries if Set exceeds limit to prevent unbounded memory growth + while (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { + const iter = this.processedMentions.values(); + const { value, done } = iter.next(); + if (done) { + break; + } + this.processedMentions.delete(value); + } } } catch (error) { console.error('❌ Error in processing loop:', error); diff --git a/src/services/grok.ts b/src/services/grok.ts index 6b35a50..a3bcf1a 100644 --- a/src/services/grok.ts +++ b/src/services/grok.ts @@ -59,7 +59,7 @@ export class GrokService { throw new Error(`Grok API error: ${response.status}`); } - const data: any = await response.json(); + const data = await response.json() as { choices: Array<{ message?: { content?: string } }> }; const analysisText = data.choices[0]?.message?.content || ''; // Use the mention post ID to reply to the specific post that mentioned the agent diff --git a/src/services/xapi.ts b/src/services/xapi.ts index 1f62056..d3fe2f8 100644 --- a/src/services/xapi.ts +++ b/src/services/xapi.ts @@ -44,16 +44,27 @@ export class XAPIClient { throw new Error('Failed to get user ID from response'); } - const mentionsResponse = await this.makeXAPIRequest( - `https://api.twitter.com/2/users/${userId}/mentions?max_results=10&expansions=author_id&tweet.fields=created_at,conversation_id,in_reply_to_user_id,referenced_tweets`, - 'GET' - ); + const params = new URLSearchParams({ + max_results: '10', + expansions: 'author_id', + 'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,referenced_tweets', + }); + if (this.lastMentionId) { + params.set('since_id', this.lastMentionId); + } + const mentionsUrl = `https://api.twitter.com/2/users/${userId}/mentions?${params.toString()}`; + const mentionsResponse = await this.makeXAPIRequest(mentionsUrl, 'GET'); if (!mentionsResponse || !Array.isArray(mentionsResponse.data)) { console.warn('Invalid response from X API (mentions)'); return []; } + // Track newest mention ID for since_id on next poll (API returns newest first) + if (mentionsResponse.data.length > 0) { + this.lastMentionId = mentionsResponse.data[0].id; + } + return this.parseMentions(mentionsResponse.data); } catch (error) { console.error('Error fetching mentions:', error); @@ -77,7 +88,17 @@ export class XAPIClient { 'GET' ); - return this.parseThread(response.data || []); + if (!response.data) { + console.warn('No data in thread response from X API'); + return null; + } + + if (!Array.isArray(response.data)) { + console.warn('Unexpected response shape from X API (thread): data is not an array'); + return null; + } + + return this.parseThread(response.data); } catch (error) { console.error('Error fetching thread:', error); return null; @@ -127,7 +148,10 @@ export class XAPIClient { 'GET' ); - return (response.data || []).map((tweet: any) => this.parsePost(tweet)); + if (!Array.isArray(response.data)) { + return []; + } + return response.data.map((tweet: Record) => this.parsePost(tweet)); } catch (error) { console.error('Error searching tweets:', error); return []; @@ -160,31 +184,31 @@ export class XAPIClient { return response.json(); } - private parseMentions(tweets: any[]): Mention[] { + private parseMentions(tweets: Record[]): Mention[] { return tweets.map((tweet) => ({ post: this.parsePost(tweet), - mentioned_at: new Date(tweet.created_at), + mentioned_at: new Date(tweet.created_at as string), processed: false, })); } - private parsePost(tweet: any): XPost { + private parsePost(tweet: Record): XPost { return { - id: tweet.id, - text: tweet.text, - author_id: tweet.author_id, - author_username: tweet.username || 'unknown', - created_at: tweet.created_at, - conversation_id: tweet.conversation_id, - in_reply_to_user_id: tweet.in_reply_to_user_id, - referenced_tweets: tweet.referenced_tweets, + id: tweet.id as string, + text: tweet.text as string, + author_id: tweet.author_id as string, + author_username: (tweet.username as string) || 'unknown', + created_at: tweet.created_at as string, + conversation_id: tweet.conversation_id as string | undefined, + in_reply_to_user_id: tweet.in_reply_to_user_id as string | undefined, + referenced_tweets: tweet.referenced_tweets as Array<{ type: string; id: string }> | undefined, }; } - private parseThread(tweets: any[]): XThread | null { + private parseThread(tweets: { created_at: string; [key: string]: unknown }[]): XThread | null { if (tweets.length === 0) return null; - const sorted = tweets.sort((a, b) => + const sorted = [...tweets].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() );