Skip to content
Closed
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
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Comment on lines 11 to 14
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
14 changes: 14 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,19 @@ 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++) {
const { value, done } = iter.next();
if (done) {
break;
}
this.processedMentions.delete(value);
}
Comment on lines 100 to 110
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

The pruning logic deletes the first values in the Set, which are the earliest inserted IDs, not necessarily the oldest mention by time. Since mentions appear to be processed in the API response order (and fetchMentions treats index 0 as “newest”), this can end up pruning newer mentions first. Either process mentions oldest→newest before inserting into processedMentions, or change the wording/approach so the eviction policy matches the intended behavior.

Copilot uses AI. Check for mistakes.
}
} catch (error) {
console.error('❌ Error in processing loop:', error);
} finally {
Expand Down
35 changes: 27 additions & 8 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,17 @@ export class XAPIClient {
'GET'
);

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

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

fetchThread returns early when response.data is falsy, but it still passes through cases where response.data is present but not an array (e.g., an object from an unexpected API shape). In that case parseThread() will throw when it reads tweets.length. Please validate response.data with Array.isArray(...) (and handle the non-array case) before calling parseThread.

Suggested change
if (!Array.isArray(response.data)) {
console.warn('Unexpected response shape from X API (thread): data is not an array');
return null;
}

Copilot uses AI. Check for mistakes.
if (!Array.isArray(response.data)) {
console.warn('X API thread response data is not an array');
return null;
}

return this.parseThread(response.data);
Comment on lines 89 to 99
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 @@ -181,10 +200,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;
Comment on lines +203 to 204
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

parseThread’s parameter type still uses an index signature with any, which weakens type safety under strict mode and makes it easier for unexpected shapes to slip through (especially since this method assumes an array-like input). Consider using unknown instead of any for the extra fields (or define a minimal tweet shape type) to avoid accidentally relying on untyped properties.

Copilot uses AI. Check for mistakes.

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