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
2 changes: 2 additions & 0 deletions .github/workflows/auto-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Auto Label
on:
pull_request:
types: [opened, reopened, synchronized]
permissions:
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Issue Triage
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
triage:
runs-on: ubuntu-latest
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: PR Checks
on:
pull_request:
branches: [main, master]
permissions:
pull-requests: write
jobs:
validate:
runs-on: ubuntu-latest
Expand All @@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
14 changes: 12 additions & 2 deletions src/services/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions src/services/grok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 24 additions & 4 deletions src/services/xapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 [];
Expand Down Expand Up @@ -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) =>
Expand Down