From 8c15aeddf9f6701b55ebc4d45fbb6546b7ffdee1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:51:30 +0000 Subject: [PATCH 1/4] Initial plan From 9ba3b57eaaa6fbb248c1a9e4d362e245bea9b2f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:55:41 +0000 Subject: [PATCH 2/4] Initial plan Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- package-lock.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9358015..ced4e7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -396,6 +396,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", @@ -600,6 +601,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -1170,6 +1172,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" } From 9f4d1595769502e26a402e03fa232d5abf81172d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:57:22 +0000 Subject: [PATCH 3/4] Implement review suggestions from PR #26 and PR #6 PR #26 fixes (with review feedback incorporated): - Redirect console.log to stderr for MCP stdout (unknown[] type) - Use URLSearchParams for mentions URL + wire up since_id - Add Array.isArray() guard in fetchThread - Copy tweets array before sorting + unknown type in parseThread - Cap processedMentions with safe {value, done} iterator - Process mentions oldest-first for chronological pruning PR #6 fix (with review feedback): - Add mentionPostId param to analyzeAndDecide for reply targeting - Replace any type with specific type in Grok response parsing Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- src/examples.ts | 4 ++-- src/index.ts | 4 ++++ src/services/agent.ts | 21 ++++++++++++++++++--- src/services/grok.ts | 18 +++++++++--------- src/services/xapi.ts | 40 ++++++++++++++++++++++++++++++++-------- 5 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/examples.ts b/src/examples.ts index aaf1598..3da6efe 100644 --- a/src/examples.ts +++ b/src/examples.ts @@ -33,7 +33,7 @@ async function example1_fetchAndAnalyzeMention() { console.log(`\nThread has ${thread.replies.length + 1} posts`); // Analyze with Grok - const analysis = await grok.analyzeAndDecide(mention.post.text, thread); + const analysis = await grok.analyzeAndDecide(mention.post.text, thread, mention.post.id); console.log(`\nGrok's Decision:`); console.log(` Action: ${analysis.action.type}`); @@ -139,7 +139,7 @@ async function example5_batchProcessMentions() { const thread = await xClient.fetchThread(conversationId); if (thread) { - const analysis = await grok.analyzeAndDecide(mention.post.text, thread); + const analysis = await grok.analyzeAndDecide(mention.post.text, thread, mention.post.id); console.log(` → Action: ${analysis.action.type} (${(analysis.confidence * 100).toFixed(0)}% confidence)`); // In a real scenario, you might execute the action here diff --git a/src/index.ts b/src/index.ts index 6dd2dfe..9ae06a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,10 @@ import { AutonomousAgent } from './services/agent.js'; import { XMCPServer } from './mcp/server.js'; async function main() { + // Redirect console.log to stderr so it doesn't conflict with + // MCP StdioServerTransport which uses stdout for protocol messages + console.log = (...args: unknown[]) => console.error(...args); + console.log('═══════════════════════════════════════════════════'); console.log(' MyXstack - Autonomous AI Agent on X (Twitter)'); console.log('═══════════════════════════════════════════════════\n'); diff --git a/src/services/agent.ts b/src/services/agent.ts index ab40033..a8cd8fa 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -11,6 +11,7 @@ export class AutonomousAgent { private grokService: GrokService; private config: AgentConfig; private processedMentions: Set = new Set(); + private static readonly MAX_PROCESSED_MENTIONS = 10000; private isRunning: boolean = false; private pollingIntervalId: NodeJS.Timeout | null = null; private isProcessing: boolean = false; @@ -90,11 +91,24 @@ 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 each mention (oldest first for proper chronological pruning) + for (const mention of [...newMentions].reverse()) { await this.processMention(mention); this.processedMentions.add(mention.post.id); } + + // Prune oldest entries to prevent unbounded memory growth + if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { + const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS; + const iter = this.processedMentions.values(); + for (let i = 0; i < excess; i++) { + const { value, done } = iter.next(); + if (done) { + break; + } + this.processedMentions.delete(value); + } + } } catch (error) { console.error('❌ Error in processing loop:', error); } finally { @@ -129,7 +143,8 @@ export class AutonomousAgent { console.log('\n🤖 Analyzing with Grok AI...'); const analysis = await this.grokService.analyzeAndDecide( mention.post.text, - thread + thread, + mention.post.id ); console.log(` Action: ${analysis.action.type.toUpperCase()}`); diff --git a/src/services/grok.ts b/src/services/grok.ts index 0fd6b2c..ea057d0 100644 --- a/src/services/grok.ts +++ b/src/services/grok.ts @@ -23,9 +23,9 @@ export class GrokService { * @param thread - The thread context including root post and replies * @returns Analysis with recommended action */ - async analyzeAndDecide(mention: string, thread: XThread): Promise { + async analyzeAndDecide(mention: string, thread: XThread, mentionPostId: string): Promise { if (this.simulationMode) { - return this.simulateAnalysis(mention, thread); + return this.simulateAnalysis(mention, thread, mentionPostId); } try { @@ -58,15 +58,15 @@ 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 root post ID from the thread, not the mention text - return this.parseGrokResponse(analysisText, thread.root_post.id); + // Use the mention post ID so replies target the specific mention + return this.parseGrokResponse(analysisText, mentionPostId); } catch (error) { console.error('Error calling Grok API:', error); // Fallback to simulation - return this.simulateAnalysis(mention, thread); + return this.simulateAnalysis(mention, thread, mentionPostId); } } @@ -145,7 +145,7 @@ export class GrokService { /** * Simulate Grok analysis for testing */ - private simulateAnalysis(mention: string, thread: XThread): GrokAnalysis { + private simulateAnalysis(mention: string, thread: XThread, mentionPostId: string): GrokAnalysis { console.log('🤖 Simulated Grok Analysis:'); console.log(` Analyzing: "${mention}"`); @@ -159,7 +159,7 @@ export class GrokService { const analysis: GrokAnalysis = { action: { type: 'reply', - target_post_id: thread.root_post.id, + target_post_id: mentionPostId, content: 'Thanks for reaching out! I\'ve analyzed your question and here\'s my insight: Based on the context, I\'d recommend exploring this topic further. Let me know if you need more specific information!', reasoning: 'Detected a question, providing helpful response', }, @@ -174,7 +174,7 @@ export class GrokService { const analysis: GrokAnalysis = { action: { type: 'analyze', - target_post_id: thread.root_post.id, + target_post_id: mentionPostId, reasoning: 'No clear action needed, just acknowledgment', }, confidence: 0.7, diff --git a/src/services/xapi.ts b/src/services/xapi.ts index f80be1e..968d3db 100644 --- a/src/services/xapi.ts +++ b/src/services/xapi.ts @@ -44,17 +44,31 @@ 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 []; } - return this.parseMentions(mentionsResponse.data); + const mentions = this.parseMentions(mentionsResponse.data); + + // Track the newest mention ID for pagination on the next poll + if (mentionsResponse.data.length > 0) { + this.lastMentionId = mentionsResponse.data[0].id; + } + + return mentions; } catch (error) { console.error('Error fetching mentions:', error); return []; @@ -77,7 +91,17 @@ export class XAPIClient { 'GET' ); - return this.parseThread(response.data || []); + if (!response || !response.data) { + console.warn('Invalid response from X API (thread)'); + 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; @@ -181,10 +205,10 @@ export class XAPIClient { }; } - private parseThread(tweets: { created_at: string; [key: string]: 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() ); From 48ceab7e6479d8c397a32a724a6af359f7998fc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:58:09 +0000 Subject: [PATCH 4/4] Improve comment on reverse() to clarify X API returns newest-first Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- src/services/agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/agent.ts b/src/services/agent.ts index a8cd8fa..8d96bb3 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -91,7 +91,8 @@ export class AutonomousAgent { console.log(`\n📬 [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`); - // Process each mention (oldest first for proper chronological pruning) + // Process each mention (X API returns newest-first, reverse to process oldest first + // so Set insertion order matches chronological order for correct pruning) for (const mention of [...newMentions].reverse()) { await this.processMention(mention); this.processedMentions.add(mention.post.id);