diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index cab0666..4b3acba 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -2,6 +2,8 @@ name: Auto Label on: pull_request: types: [opened, reopened, synchronized] +permissions: + pull-requests: write jobs: label: runs-on: ubuntu-latest diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 9379a8d..7ad9636 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -2,6 +2,8 @@ name: Issue Triage on: issues: types: [opened] +permissions: + issues: write jobs: triage: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 2464d70..7e2de71 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -2,6 +2,8 @@ name: PR Checks on: pull_request: branches: [main, master] +permissions: + pull-requests: write jobs: validate: runs-on: ubuntu-latest @@ -10,4 +12,4 @@ jobs: - name: Basic Syntax Check run: | echo "Running syntax validation..." - find . -name "*.js" -o -name "*.py" -o -name "*.ts" | xargs -I {} node -c {} || true + find . -name "*.js" -o -name "*.ts" | xargs -I {} node -c {} || true diff --git a/README.md b/README.md index 0dd0e21..e751f94 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MyXstack -This repository hosts a lightweight, step-by-step guide for setting up an autonomous X (Twitter) agent system that acts based on thread context & reasoning, through Grok via the xMCP server. +This repository hosts a lightweight, step-by-step guide for setting up an autonomous X (Twitter) agent system that acts on thread context and reasoning, through Grok via the xMCP server. ## Phase 1: Gather prerequisites & accounts (1–2 hours) diff --git a/src/services/agent.ts b/src/services/agent.ts index ab40033..62e0fc0 100644 --- a/src/services/agent.ts +++ b/src/services/agent.ts @@ -90,11 +90,21 @@ 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 mentions oldest-first (X API returns newest first) + for (const mention of [...newMentions].reverse()) { await this.processMention(mention); this.processedMentions.add(mention.post.id); } + + // Prune oldest processed mentions to prevent unbounded growth + const iter = this.processedMentions.values(); + while (this.processedMentions.size > 1000) { + const { value, done } = iter.next(); + if (done) { + break; + } + this.processedMentions.delete(value); + } } catch (error) { console.error('āŒ Error in processing loop:', error); } finally { diff --git a/src/services/grok.ts b/src/services/grok.ts index 0fd6b2c..79f1746 100644 --- a/src/services/grok.ts +++ b/src/services/grok.ts @@ -58,7 +58,7 @@ 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 @@ -115,7 +115,7 @@ export class GrokService { const parsed = JSON.parse(jsonMatch[0]); const action: AgentAction = { - type: parsed.action as any, + type: parsed.action as AgentAction['type'], target_post_id: mentionPostId, content: parsed.content, query: parsed.action === 'search' ? parsed.content : undefined, diff --git a/src/services/xapi.ts b/src/services/xapi.ts index f80be1e..1fa2f3c 100644 --- a/src/services/xapi.ts +++ b/src/services/xapi.ts @@ -44,8 +44,16 @@ export class XAPIClient { throw new Error('Failed to get user ID from response'); } + 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 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`, + `https://api.twitter.com/2/users/${userId}/mentions?${params.toString()}`, 'GET' ); @@ -77,7 +85,16 @@ export class XAPIClient { 'GET' ); - return this.parseThread(response.data || []); + if (!response.data) { + 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; @@ -127,7 +144,10 @@ export class XAPIClient { 'GET' ); - return (response.data || []).map((tweet: any) => this.parsePost(tweet)); + if (!Array.isArray(response.data)) { + return []; + } + return response.data.map((tweet: any) => this.parsePost(tweet)); } catch (error) { console.error('Error searching tweets:', error); return []; @@ -181,7 +201,7 @@ 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) =>