Skip to content

Commit 66d9f9b

Browse files
fix: consolidate actionable review feedback from PRs #4, #6, #26
- Add console.log→stderr redirect with unknown[] type (PR #26) - Use URLSearchParams for mentions URL + since_id pagination (PR #26) - Add Array.isArray() guard in fetchThread() (PR #26) - Add Set pruning with safe { value, done } iterator (PR #26) - Process mentions oldest-first for chronological pruning (PR #26) - Use [key: string]: unknown in parseThread, spread sort (PR #26) - Add mentionPostId param for correct reply targeting (PR #6) - Fix any type in Grok response parsing (PR #6) - Fix README broken list numbering (PR #4) Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com>
1 parent 41e731b commit 66d9f9b

6 files changed

Lines changed: 68 additions & 23 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ This repository hosts a lightweight, step-by-step guide for setting up an autono
2020
3. Note the bot user ID (you can fetch this via the API later).
2121

2222
### xAI / Grok API key
23-
. Visit <https://console.x.ai>, open the API keys section, and create a key that starts with `xai-`.
24-
. Store the key securely.
23+
1. Visit <https://console.x.ai>, open the API keys section, and create a key that starts with `xai-`.
24+
2. Store the key securely.
2525

2626
### Local tooling
2727
- Install Python 3.9+ (3.10–3.13 recommended).

src/examples.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ async function example1_fetchAndAnalyzeMention() {
3333
console.log(`\nThread has ${thread.replies.length + 1} posts`);
3434

3535
// Analyze with Grok
36-
const analysis = await grok.analyzeAndDecide(mention.post.text, thread);
36+
const analysis = await grok.analyzeAndDecide(mention.post.text, thread, mention.post.id);
3737

3838
console.log(`\nGrok's Decision:`);
3939
console.log(` Action: ${analysis.action.type}`);
@@ -139,7 +139,7 @@ async function example5_batchProcessMentions() {
139139
const thread = await xClient.fetchThread(conversationId);
140140

141141
if (thread) {
142-
const analysis = await grok.analyzeAndDecide(mention.post.text, thread);
142+
const analysis = await grok.analyzeAndDecide(mention.post.text, thread, mention.post.id);
143143
console.log(` → Action: ${analysis.action.type} (${(analysis.confidence * 100).toFixed(0)}% confidence)`);
144144

145145
// In a real scenario, you might execute the action here

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { AutonomousAgent } from './services/agent.js';
88
import { XMCPServer } from './mcp/server.js';
99

1010
async function main() {
11+
// Redirect console.log to stderr so it doesn't conflict with
12+
// MCP StdioServerTransport which uses stdout for protocol messages
13+
console.log = (...args: unknown[]) => console.error(...args);
14+
1115
console.log('═══════════════════════════════════════════════════');
1216
console.log(' MyXstack - Autonomous AI Agent on X (Twitter)');
1317
console.log('═══════════════════════════════════════════════════\n');

src/services/agent.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class AutonomousAgent {
1111
private grokService: GrokService;
1212
private config: AgentConfig;
1313
private processedMentions: Set<string> = new Set();
14+
private static readonly MAX_PROCESSED_MENTIONS = 10000;
1415
private isRunning: boolean = false;
1516
private pollingIntervalId: NodeJS.Timeout | null = null;
1617
private isProcessing: boolean = false;
@@ -91,10 +92,24 @@ export class AutonomousAgent {
9192
console.log(`\n📬 [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`);
9293

9394
// Process each mention
94-
for (const mention of newMentions) {
95+
// Process oldest first so chronological order is preserved in the Set
96+
for (const mention of [...newMentions].reverse()) {
9597
await this.processMention(mention);
9698
this.processedMentions.add(mention.post.id);
9799
}
100+
101+
// Prune oldest entries to prevent unbounded memory growth
102+
if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) {
103+
const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS;
104+
const iter = this.processedMentions.values();
105+
for (let i = 0; i < excess; i++) {
106+
const { value, done } = iter.next();
107+
if (done) {
108+
break;
109+
}
110+
this.processedMentions.delete(value);
111+
}
112+
}
98113
} catch (error) {
99114
console.error('❌ Error in processing loop:', error);
100115
} finally {
@@ -129,7 +144,8 @@ export class AutonomousAgent {
129144
console.log('\n🤖 Analyzing with Grok AI...');
130145
const analysis = await this.grokService.analyzeAndDecide(
131146
mention.post.text,
132-
thread
147+
thread,
148+
mention.post.id
133149
);
134150

135151
console.log(` Action: ${analysis.action.type.toUpperCase()}`);

src/services/grok.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ export class GrokService {
2121
* Analyze a mention and thread context to determine appropriate action
2222
* @param mention - The text content of the mention to analyze
2323
* @param thread - The thread context including root post and replies
24+
* @param mentionPostId - The ID of the post where the agent was mentioned
2425
* @returns Analysis with recommended action
2526
*/
26-
async analyzeAndDecide(mention: string, thread: XThread): Promise<GrokAnalysis> {
27+
async analyzeAndDecide(mention: string, thread: XThread, mentionPostId: string): Promise<GrokAnalysis> {
2728
if (this.simulationMode) {
28-
return this.simulateAnalysis(mention, thread);
29+
return this.simulateAnalysis(mention, thread, mentionPostId);
2930
}
3031

3132
try {
@@ -58,15 +59,15 @@ export class GrokService {
5859
throw new Error(`Grok API error: ${response.status}`);
5960
}
6061

61-
const data: any = await response.json();
62+
const data = await response.json() as { choices: { message?: { content?: string } }[] };
6263
const analysisText = data.choices[0]?.message?.content || '';
6364

64-
// Use the root post ID from the thread, not the mention text
65-
return this.parseGrokResponse(analysisText, thread.root_post.id);
65+
// Use the mention post ID to reply to the specific post that mentioned the agent
66+
return this.parseGrokResponse(analysisText, mentionPostId);
6667
} catch (error) {
6768
console.error('Error calling Grok API:', error);
6869
// Fallback to simulation
69-
return this.simulateAnalysis(mention, thread);
70+
return this.simulateAnalysis(mention, thread, mentionPostId);
7071
}
7172
}
7273

@@ -145,7 +146,7 @@ export class GrokService {
145146
/**
146147
* Simulate Grok analysis for testing
147148
*/
148-
private simulateAnalysis(mention: string, thread: XThread): GrokAnalysis {
149+
private simulateAnalysis(mention: string, thread: XThread, mentionPostId: string): GrokAnalysis {
149150
console.log('🤖 Simulated Grok Analysis:');
150151
console.log(` Analyzing: "${mention}"`);
151152

@@ -159,7 +160,7 @@ export class GrokService {
159160
const analysis: GrokAnalysis = {
160161
action: {
161162
type: 'reply',
162-
target_post_id: thread.root_post.id,
163+
target_post_id: mentionPostId,
163164
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!',
164165
reasoning: 'Detected a question, providing helpful response',
165166
},
@@ -174,7 +175,7 @@ export class GrokService {
174175
const analysis: GrokAnalysis = {
175176
action: {
176177
type: 'analyze',
177-
target_post_id: thread.root_post.id,
178+
target_post_id: mentionPostId,
178179
reasoning: 'No clear action needed, just acknowledgment',
179180
},
180181
confidence: 0.7,

src/services/xapi.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,31 @@ export class XAPIClient {
4444
throw new Error('Failed to get user ID from response');
4545
}
4646

47-
const mentionsResponse = await this.makeXAPIRequest(
48-
`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`,
49-
'GET'
50-
);
47+
const params = new URLSearchParams({
48+
max_results: '10',
49+
expansions: 'author_id',
50+
'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,referenced_tweets',
51+
});
52+
if (this.lastMentionId) {
53+
params.set('since_id', this.lastMentionId);
54+
}
55+
const mentionsUrl = `https://api.twitter.com/2/users/${userId}/mentions?${params.toString()}`;
56+
57+
const mentionsResponse = await this.makeXAPIRequest(mentionsUrl, 'GET');
5158

5259
if (!mentionsResponse || !Array.isArray(mentionsResponse.data)) {
5360
console.warn('Invalid response from X API (mentions)');
5461
return [];
5562
}
5663

57-
return this.parseMentions(mentionsResponse.data);
64+
const mentions = this.parseMentions(mentionsResponse.data);
65+
66+
// Track the newest mention ID for pagination on the next poll
67+
if (mentionsResponse.data.length > 0) {
68+
this.lastMentionId = mentionsResponse.data[0].id;
69+
}
70+
71+
return mentions;
5872
} catch (error) {
5973
console.error('Error fetching mentions:', error);
6074
return [];
@@ -77,7 +91,17 @@ export class XAPIClient {
7791
'GET'
7892
);
7993

80-
return this.parseThread(response.data || []);
94+
if (!response || !response.data) {
95+
console.warn('Invalid response from X API (thread)');
96+
return null;
97+
}
98+
99+
if (!Array.isArray(response.data)) {
100+
console.warn('Unexpected response shape from X API (thread): data is not an array');
101+
return null;
102+
}
103+
104+
return this.parseThread(response.data);
81105
} catch (error) {
82106
console.error('Error fetching thread:', error);
83107
return null;
@@ -181,10 +205,10 @@ export class XAPIClient {
181205
};
182206
}
183207

184-
private parseThread(tweets: any[]): XThread | null {
208+
private parseThread(tweets: { created_at: string; [key: string]: unknown }[]): XThread | null {
185209
if (tweets.length === 0) return null;
186210

187-
const sorted = tweets.sort((a, b) =>
211+
const sorted = [...tweets].sort((a, b) =>
188212
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
189213
);
190214

0 commit comments

Comments
 (0)