Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ 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
const origLog = console.log;
console.log = (...args: any[]) => console.error(...args);
Comment on lines +13 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The origLog variable is declared but never used and can be removed.

While redirecting console.log to stderr works, be aware that modifying global objects (monkey-patching) can have unintended side effects in larger applications or when using third-party libraries that might not expect this behavior. It can also complicate debugging. For a more robust solution in the future, consider using a dedicated logging library which allows for flexible stream configuration.

Suggested change
const origLog = console.log;
console.log = (...args: any[]) => console.error(...args);
console.log = (...args: any[]) => console.error(...args);

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type.

Suggested change
console.log = (...args: any[]) => console.error(...args);
console.log = (...args: unknown[]) => console.error(...args);

Copilot uses AI. Check for mistakes.

Comment on lines +11 to +15
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

origLog is assigned but never used. Either remove it to avoid dead code, or use it to restore console.log during shutdown/after MCP server init (depending on the intended lifecycle).

Copilot uses AI. Check for mistakes.
console.log('═══════════════════════════════════════════════════');
console.log(' MyXstack - Autonomous AI Agent on X (Twitter)');
console.log('═══════════════════════════════════════════════════\n');
Expand Down
10 changes: 10 additions & 0 deletions src/services/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class AutonomousAgent {
private grokService: GrokService;
private config: AgentConfig;
private processedMentions: Set<string> = new Set();
private static readonly MAX_PROCESSED_MENTIONS = 10000;
private isRunning: boolean = false;
private pollingIntervalId: NodeJS.Timeout | null = null;
private isProcessing: boolean = false;
Expand Down Expand Up @@ -95,6 +96,15 @@ export class AutonomousAgent {
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++) {
this.processedMentions.delete(iter.next().value as string);
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pruning loop deletes iter.next().value as string, which relies on a type assertion and would silently pass undefined if iteration were ever exhausted. Prefer reading { value, done } from iter.next() and breaking if done, which keeps the code type-safe and avoids the assertion.

Suggested change
this.processedMentions.delete(iter.next().value as string);
const { value, done } = iter.next();
if (done) {
break;
}
this.processedMentions.delete(value);

Copilot uses AI. Check for mistakes.
}
}
} catch (error) {
console.error('❌ Error in processing loop:', error);
} finally {
Expand Down
28 changes: 21 additions & 7 deletions src/services/xapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,26 @@ 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'
);
let mentionsUrl = `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`;
if (this.lastMentionId) {
mentionsUrl += `&since_id=${this.lastMentionId}`;
}
Comment on lines +47 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Building URLs via string concatenation can be fragile and hard to read. Using URLSearchParams provides a more robust and maintainable way to construct URLs with query parameters, as it handles proper encoding and separation of parameters automatically.

      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 [];
Expand All @@ -77,7 +86,12 @@ export class XAPIClient {
'GET'
);

return this.parseThread(response.data || []);
if (!response || !response.data) {
console.warn('Invalid response from X API (thread)');
return null;
}

return this.parseThread(response.data);
Comment on lines +89 to +94
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchThread() now only checks response.data for truthiness, but parseThread() expects an array and will still throw if response.data is a non-array object (which can happen with unexpected API shapes). Consider guarding with Array.isArray(response.data) (and returning null otherwise) before calling parseThread().

Copilot uses AI. Check for mistakes.
} catch (error) {
console.error('Error fetching thread:', error);
return null;
Expand Down Expand Up @@ -184,7 +198,7 @@ export class XAPIClient {
private parseThread(tweets: { created_at: string; [key: string]: any }[]): XThread | null {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected any. Specify a different type.

Suggested change
private parseThread(tweets: { created_at: string; [key: string]: any }[]): XThread | null {
private parseThread(tweets: { created_at: string; [key: string]: unknown }[]): XThread | null {

Copilot uses AI. Check for mistakes.
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()
);

Expand Down
Loading