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: 0 additions & 2 deletions .github/workflows/auto-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,3 @@ jobs:
uses: actions/labeler@v5
with:
sync-labels: true
with:
sync-labels: true
6 changes: 3 additions & 3 deletions 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 based on thread context and reasoning, through Grok via the xMCP server.

## Phase 1: Gather prerequisites & accounts (1–2 hours)

Expand All @@ -20,8 +20,8 @@ This repository hosts a lightweight, step-by-step guide for setting up an autono
3. Note the bot user ID (you can fetch this via the API later).

### xAI / Grok API key
. Visit <https://console.x.ai>, open the API keys section, and create a key that starts with `xai-`.
. Store the key securely.
1. Visit <https://console.x.ai>, open the API keys section, and create a key that starts with `xai-`.
2. Store the key securely.

### Local tooling
- Install Python 3.9+ (3.10–3.13 recommended).
Expand Down
17 changes: 15 additions & 2 deletions src/services/grok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,21 @@ export class GrokService {
throw new Error(`Grok API error: ${response.status}`);
}

const data: any = await response.json();
const analysisText = data.choices[0]?.message?.content || '';
const data: unknown = await response.json();

// Validate and narrow response structure
if (
typeof data !== 'object' ||
data === null ||
!('choices' in data) ||
!Array.isArray(data.choices) ||
data.choices.length === 0
) {
throw new Error('Invalid response structure from Grok API: missing or empty choices array');
}

const typedData = data as { choices: Array<{ message?: { content?: string } }> };
const analysisText = typedData.choices[0]?.message?.content || '';

// Use the root post ID from the thread, not the mention text
return this.parseGrokResponse(analysisText, thread.root_post.id);
Expand Down
22 changes: 16 additions & 6 deletions src/services/xapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,16 @@ 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)');
Expand Down Expand Up @@ -77,7 +83,11 @@ export class XAPIClient {
'GET'
);

return this.parseThread(response.data || []);
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 @@ -181,7 +191,7 @@ 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) =>
Expand Down