Skip to content
Merged
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
22 changes: 11 additions & 11 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# CODEOWNERS - Automatically assigns reviewers based on file paths

# Default owner for everything
* @codex
* @groupthinking

# Frontend
*.tsx @Vercel
*.jsx @Vercel
*.css @Vercel
/frontend/ @Vercel
*.tsx @groupthinking
*.jsx @groupthinking
*.css @groupthinking
/frontend/ @groupthinking

# Backend
*.py @groupthinking
Expand All @@ -16,11 +16,11 @@
/api/ @groupthinking

# Infrastructure
/.github/ @Claude
*.yml @Claude
*.yaml @Claude
Dockerfile @Claude
/.github/ @groupthinking
*.yml @groupthinking
*.yaml @groupthinking
Dockerfile @groupthinking

# Documentation
*.md @Copilot
/docs/ @Copilot
*.md @groupthinking
/docs/ @groupthinking
5 changes: 5 additions & 0 deletions .github/workflows/auto-label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: Auto Label
on:
pull_request:
types: [opened, reopened, synchronized]

permissions:
contents: read
pull-requests: write

Comment on lines +8 to +9
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.

This workflow uses github.rest.issues.addLabels/listLabelsForRepo, which require issues scope. The current permissions block omits issues: write, so labeling can fail with insufficient permissions. Update permissions to include issues: write (and pull-requests: read is sufficient for pulls.listFiles).

Suggested change
pull-requests: write
pull-requests: read
issues: write

Copilot uses AI. Check for mistakes.
jobs:
label:
runs-on: ubuntu-latest
Expand Down
21 changes: 18 additions & 3 deletions .github/workflows/issue-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ name: Issue Triage
on:
issues:
types: [opened]

permissions:
issues: write

jobs:
triage:
runs-on: ubuntu-latest
Expand All @@ -22,12 +26,23 @@ jobs:
labels.push('needs-triage');

if (labels.length > 0) {
await github.rest.issues.addLabels({
// Fetch existing labels to avoid "Label does not exist" errors
const { data: repoLabels } = await github.rest.issues.listLabelsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labels
per_page: 100,
});
const existingLabelNames = new Set(repoLabels.map(l => l.name));
const labelsToAdd = labels.filter(l => existingLabelNames.has(l));

if (labelsToAdd.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labelsToAdd
});
}
}

await github.rest.issues.createComment({
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: PR Checks
on:
pull_request:
types: [opened, reopened, synchronize, edited]

permissions:
contents: read
pull-requests: write

Comment on lines +8 to +9
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 workflow calls github.rest.issues.createComment, which requires issues: write permissions (PRs are issues under the hood). With the current permissions block lacking issues: write, the action may fail with a 403 when trying to comment on the PR. Add issues: write (and consider reducing pull-requests to read if you’re aiming for least-privilege).

Suggested change
pull-requests: write
pull-requests: read
issues: write

Copilot uses AI. Check for mistakes.
jobs:
validate:
runs-on: ubuntu-latest
Expand Down
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);

console.log('═══════════════════════════════════════════════════');
console.log(' MyXstack - Autonomous AI Agent on X (Twitter)');
console.log('═══════════════════════════════════════════════════\n');
Expand Down
18 changes: 14 additions & 4 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 @@ -90,10 +91,19 @@ export class AutonomousAgent {

console.log(`\n📬 [${new Date().toLocaleTimeString()}] Found ${newMentions.length} new mention(s)!\n`);

// Process each mention
for (const mention of newMentions) {
await this.processMention(mention);
this.processedMentions.add(mention.post.id);
// Process mentions oldest-first (API returns newest first) to maintain
// chronological insertion order in the Set for proper pruning behavior
for (let i = newMentions.length - 1; i >= 0; i--) {
await this.processMention(newMentions[i]);
this.processedMentions.add(newMentions[i].post.id);
}

// Prune oldest entries (from beginning of Set) to prevent unbounded memory growth
// Sets maintain insertion order, so slice(0, excess) removes oldest entries
if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) {
const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS;
const toDelete = Array.from(this.processedMentions).slice(0, excess);
toDelete.forEach(id => this.processedMentions.delete(id));
Comment on lines +102 to +106
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.

Pruning processedMentions by Array.from(set).slice(...) copies the entire Set to an array each time pruning triggers (once the Set reaches 10k, this can happen on every poll). Prefer deleting the first excess entries via the Set iterator to avoid allocating an O(n) array and to keep pruning O(excess).

Suggested change
// Sets maintain insertion order, so slice(0, excess) removes oldest entries
if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) {
const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS;
const toDelete = Array.from(this.processedMentions).slice(0, excess);
toDelete.forEach(id => this.processedMentions.delete(id));
// Sets maintain insertion order, so we remove the first `excess` entries via the iterator
if (this.processedMentions.size > AutonomousAgent.MAX_PROCESSED_MENTIONS) {
const excess = this.processedMentions.size - AutonomousAgent.MAX_PROCESSED_MENTIONS;
let removed = 0;
for (const id of this.processedMentions) {
if (removed >= excess) {
break;
}
this.processedMentions.delete(id);
removed += 1;
}

Copilot uses AI. Check for mistakes.
}
} catch (error) {
console.error('❌ Error in processing loop:', error);
Expand Down
10 changes: 9 additions & 1 deletion src/services/grok.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
*/
import { XThread, GrokAnalysis, AgentAction } from '../types/index.js';

interface GrokApiResponse {
choices: Array<{
message?: {
content?: string;
};
}>;
}

export class GrokService {
private apiKey: string;
private simulationMode: boolean = false;
Expand Down Expand Up @@ -59,7 +67,7 @@ export class GrokService {
throw new Error(`Grok API error: ${response.status}`);
}

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

Comment on lines +70 to 72
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.

response.json() is cast to GrokApiResponse, but the code then assumes data.choices exists. If the API returns an unexpected success payload (or an upstream proxy alters it), this will throw at runtime. Add a small runtime guard (e.g., verify data is an object and Array.isArray(data.choices)) before reading choices[0].

Copilot uses AI. Check for mistakes.
// Use the mention post ID so replies target the specific post where the agent was mentioned
Expand Down
41 changes: 33 additions & 8 deletions src/services/xapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { XPost, XThread, Mention, XAPIConfig } from '../types/index.js';

export class XAPIClient {
private config: XAPIConfig;
// Track the most recent mention ID to enable pagination (avoid re-fetching)
private lastMentionId: string | null = null;
private simulationMode: boolean = false;

Expand Down Expand Up @@ -44,17 +45,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 && typeof mentionsResponse.data[0].id === 'string') {
this.lastMentionId = mentionsResponse.data[0].id;
}

return mentions;
} catch (error) {
console.error('Error fetching mentions:', error);
return [];
Expand All @@ -77,7 +92,17 @@ export class XAPIClient {
'GET'
);

return this.parseThread(response.data || []);
if (!response || !response.data) {
console.warn('Invalid response from X API (thread)');
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 @@ -181,10 +206,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;

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
Loading