From 7654c13393dc11034064c4546a2a4710903c11f8 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:10 +0000 Subject: [PATCH 1/8] Initial plan From 354adb4221555e4f332fe5fdff9d868906ab22df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:07:23 +0000 Subject: [PATCH 2/8] Consolidate review feedback from PRs #26, #6, and #13 This commit applies all actionable review feedback from multiple pending PRs: ## PR #26 - Console.log redirect and type improvements - Added console.log redirect to stderr in main() to prevent MCP protocol conflicts - Used unknown[] instead of any[] for type safety - Refactored fetchMentions() to use URLSearchParams for cleaner URL construction - Added lastMentionId tracking for efficient pagination - Added Array.isArray guard in fetchThread() for safer data handling - Changed parseThread() to use proper typed parameters and copy array before sort ## PR #6 - Pass mention post ID for accurate replies - Added mentionPostId parameter to analyzeAndDecide() and simulateAnalysis() - Updated all callers to pass mention.post.id - Fixed response.json() type with proper type assertion - Fixed parseGrokResponse to use mentionPostId instead of root_post.id - Updated examples.ts to pass mention.post.id ## PR #13 - GitHub Actions workflows with review feedback - Created .github/workflows/auto-label.yml for PR auto-labeling - Created .github/workflows/pr-checks.yml for CI builds - Created .github/workflows/issue-triage.yml for automatic issue triage - Created .github/labeler.yml configuration for auto-labeling - All workflows use secure permissions and latest action versions ## Additional improvements from review feedback - Added MAX_PROCESSED_MENTIONS constant (10000) to prevent unbounded memory growth - Reversed mention processing order (oldest-first) for better chronological handling - Added safe pruning logic using iterator pattern to cap Set size - Improved type safety throughout with unknown[] and proper type assertions All changes compile successfully with TypeScript. No tests to run. --- .github/labeler.yml | 25 +++++++++++++++++++ .github/workflows/auto-label.yml | 15 ++++++++++++ .github/workflows/issue-triage.yml | 39 ++++++++++++++++++++++++++++++ .github/workflows/pr-checks.yml | 20 +++++++++++++++ src/examples.ts | 4 +-- src/index.ts | 4 +++ src/services/agent.ts | 21 +++++++++++++--- src/services/grok.ts | 18 +++++++------- src/services/xapi.ts | 35 +++++++++++++++++++++------ 9 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/auto-label.yml create mode 100644 .github/workflows/issue-triage.yml create mode 100644 .github/workflows/pr-checks.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..54d64d4 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,25 @@ +documentation: + - changed-files: + - any-glob-to-any-file: + - '**/*.md' + - 'docs/**' + +source: + - changed-files: + - any-glob-to-any-file: + - 'src/**' + - '**/*.ts' + +config: + - changed-files: + - any-glob-to-any-file: + - '*.json' + - '.env*' + - 'tsconfig.json' + +workflows: + - changed-files: + - any-glob-to-any-file: + - '.github/**' + - '**/*.yml' + - '**/*.yaml' diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000..b3f76f4 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,15 @@ +name: Auto Label PRs +on: + pull_request_target: + types: [opened, synchronize] +permissions: + pull-requests: write + contents: read +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Label PR based on files changed + uses: actions/labeler@v5 + with: + sync-labels: true diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml new file mode 100644 index 0000000..3020c93 --- /dev/null +++ b/.github/workflows/issue-triage.yml @@ -0,0 +1,39 @@ +name: Issue Triage +on: + issues: + types: [opened] +permissions: + issues: write +jobs: + triage: + runs-on: ubuntu-latest + steps: + - name: Add triage label + uses: actions/github-script@v7 + with: + script: | + const labelName = 'needs-triage'; + // Ensure label exists + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + }); + } catch (e) { + if (e.status === 404) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: labelName, + color: 'FBCA04', + description: 'Issue needs triage', + }); + } + } + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [labelName], + }); diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..b3c4a5c --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,20 @@ +name: PR Checks +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [main] +permissions: + pull-requests: write + contents: read +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + - run: npm ci + - run: npm run build 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..7245399 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 in reverse (oldest-first) since API returns newest-first + 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..32cf58f 100644 --- a/src/services/grok.ts +++ b/src/services/grok.ts @@ -21,11 +21,12 @@ export class GrokService { * Analyze a mention and thread context to determine appropriate action * @param mention - The text content of the mention to analyze * @param thread - The thread context including root post and replies + * @param mentionPostId - The ID of the post where the agent was mentioned * @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 +59,14 @@ 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); + 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 1f62056..7e8b610 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,12 @@ export class XAPIClient { 'GET' ); - return this.parseThread(response.data || []); + if (!response || !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 +200,10 @@ export class XAPIClient { }; } - 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() ); From faea586b40624d4bb1ed59582e94d3ab8772e070 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:08:12 +0000 Subject: [PATCH 3/8] Address code review feedback - Extract max_results as MAX_MENTIONS_PER_FETCH constant in XAPIClient - Add clarifying comment about Set insertion-order iteration in pruning logic --- src/services/agent.ts | 2 ++ src/services/xapi.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/agent.ts b/src/services/agent.ts index 7245399..b03dedd 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -98,6 +98,8 @@ export class AutonomousAgent { } // Prune oldest entries to prevent unbounded memory growth + // Note: Set iteration order is insertion order in JavaScript (ES2015+) + // This removes the oldest entries (first inserted) from the Set if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS; const iter = this.processedMentions.values(); diff --git a/src/services/xapi.ts b/src/services/xapi.ts index 7e8b610..d9306be 100644 --- a/src/services/xapi.ts +++ b/src/services/xapi.ts @@ -8,6 +8,7 @@ export class XAPIClient { private config: XAPIConfig; private lastMentionId: string | null = null; private simulationMode: boolean = false; + private static readonly MAX_MENTIONS_PER_FETCH = 10; constructor(config: XAPIConfig) { this.config = config; @@ -45,7 +46,7 @@ export class XAPIClient { } const params = new URLSearchParams({ - max_results: '10', + max_results: String(XAPIClient.MAX_MENTIONS_PER_FETCH), expansions: 'author_id', 'tweet.fields': 'created_at,conversation_id,in_reply_to_user_id,referenced_tweets', }); From 02c6307e4f1a4d7a36949f7e1f34e378a27ed0e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:08:55 +0000 Subject: [PATCH 4/8] Improve performance and code clarity - Use decremented for-loop instead of array copy + reverse for efficiency - Simplify pruning logic with Array.from().slice() for better readability --- src/services/agent.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/services/agent.ts b/src/services/agent.ts index b03dedd..1d09a2a 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -92,9 +92,9 @@ export class AutonomousAgent { console.log(`\n📬 [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`); // Process in reverse (oldest-first) since API returns newest-first - for (const mention of [...newMentions].reverse()) { - await this.processMention(mention); - this.processedMentions.add(mention.post.id); + for (let i = newMentions.length - 1; i >= 0; i--) { + await this.processMention(newMentions[i]); + this.processedMentions.add(newMentions[i].post.id); } // Prune oldest entries to prevent unbounded memory growth @@ -102,14 +102,7 @@ export class AutonomousAgent { // This removes the oldest entries (first inserted) from the Set 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); - } + Array.from(this.processedMentions).slice(0, excess).forEach(id => this.processedMentions.delete(id)); } } catch (error) { console.error('❌ Error in processing loop:', error); From 9cdcdcf52255f2d604704f91a07defdfb9b2a382 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:09:29 +0000 Subject: [PATCH 5/8] Fix labeler.yml patterns for more precise matching - Remove redundant patterns (*.json already covers tsconfig.json, src/** covers all TypeScript files) - Scope workflows pattern to .github/ directory only to avoid false positives --- .github/labeler.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 54d64d4..5147a8d 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -8,18 +8,15 @@ source: - changed-files: - any-glob-to-any-file: - 'src/**' - - '**/*.ts' config: - changed-files: - any-glob-to-any-file: - '*.json' - '.env*' - - 'tsconfig.json' workflows: - changed-files: - any-glob-to-any-file: - - '.github/**' - - '**/*.yml' - - '**/*.yaml' + - '.github/**/*.yml' + - '.github/**/*.yaml' From 4304c6f244572dff05f9b35667ca172e9b855f17 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:10:14 +0000 Subject: [PATCH 6/8] Add runtime safety checks - Add optional chaining check for data[0]?.id in lastMentionId tracking - Use optional chaining for error status check in issue-triage workflow --- .github/workflows/issue-triage.yml | 2 +- src/services/xapi.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 3020c93..3ed0836 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -21,7 +21,7 @@ jobs: name: labelName, }); } catch (e) { - if (e.status === 404) { + if (e?.status === 404) { await github.rest.issues.createLabel({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/src/services/xapi.ts b/src/services/xapi.ts index d9306be..052a4c0 100644 --- a/src/services/xapi.ts +++ b/src/services/xapi.ts @@ -65,7 +65,7 @@ export class XAPIClient { const mentions = this.parseMentions(mentionsResponse.data); // Track the newest mention ID for pagination on the next poll - if (mentionsResponse.data.length > 0) { + if (mentionsResponse.data.length > 0 && mentionsResponse.data[0]?.id) { this.lastMentionId = mentionsResponse.data[0].id; } From c7faba912f5627dea44981454dfab98236bb6b43 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:10:56 +0000 Subject: [PATCH 7/8] Make labeler patterns more robust for config files - Use **/*.json to match JSON files in any directory - Use **/.env* to match .env files in any directory --- .github/labeler.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 5147a8d..14fdfac 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -12,8 +12,8 @@ source: config: - changed-files: - any-glob-to-any-file: - - '*.json' - - '.env*' + - '**/*.json' + - '**/.env*' workflows: - changed-files: From 7dbd28e18ed8010b3924dc8b538d245d511bed82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:14:58 +0000 Subject: [PATCH 8/8] fix: use iterator-based pruning to avoid O(n) Array.from() overhead Co-authored-by: groupthinking <154503486+groupthinking@users.noreply.github.com> --- src/services/agent.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/services/agent.ts b/src/services/agent.ts index 1d09a2a..6635cad 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -98,11 +98,17 @@ export class AutonomousAgent { } // Prune oldest entries to prevent unbounded memory growth - // Note: Set iteration order is insertion order in JavaScript (ES2015+) - // This removes the oldest entries (first inserted) from the Set + // Set iteration order is insertion order (ES2015+), so oldest entries come first if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) { const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS; - Array.from(this.processedMentions).slice(0, excess).forEach(id => this.processedMentions.delete(id)); + 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);