feat: Twitter/X integration (read-only free tier)

- New client: src/clients/twitter.ts
- Tools: twitter_search_tweets, twitter_get_user_profile, twitter_get_user_tweets, twitter_create_tweet
- REST endpoints: GET /api/twitter/search, /api/twitter/user, /api/twitter/tweets, POST /api/twitter/tweet
- Multi-account env var: TWITTER_{ACCOUNT}_BEARER_TOKEN
- twitter_create_tweet returns clear error about paid tier requirement

Total tools: 36
This commit is contained in:
Garfield
2026-05-05 22:11:19 -04:00
parent 136bc257d1
commit 59501f11f1
5 changed files with 394 additions and 0 deletions

136
src/clients/twitter.ts Normal file
View File

@@ -0,0 +1,136 @@
const TWITTER_API_BASE = 'https://api.twitter.com/2';
function getBearerToken(account: string): string {
const envKey = `TWITTER_${account.toUpperCase()}_BEARER_TOKEN`;
return process.env[envKey] ?? '';
}
async function twitterRequest(
endpoint: string,
bearerToken: string,
params?: Record<string, string>
) {
const url = new URL(`${TWITTER_API_BASE}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value) url.searchParams.append(key, value);
});
}
const res = await fetch(url.toString(), {
headers: {
'Authorization': `Bearer ${bearerToken}`,
},
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`Twitter API error (${res.status}): ${error}`);
}
return res.json();
}
export async function searchTweets(args: {
query: string;
max_results?: number;
account?: string;
}): Promise<Array<{
id: string;
text: string;
author_id?: string;
created_at?: string;
}>> {
const bearerToken = getBearerToken(args.account ?? 'default');
if (!bearerToken) {
throw new Error('Missing Twitter credentials. Set TWITTER_{ACCOUNT}_BEARER_TOKEN');
}
const data = await twitterRequest('/tweets/search/recent', bearerToken, {
query: args.query,
max_results: String(Math.min(args.max_results ?? 10, 100)),
'tweet.fields': 'created_at,author_id',
});
return (data.data ?? []) as Array<{ id: string; text: string; author_id?: string; created_at?: string }>;
}
export async function getUserProfile(args: {
username: string;
account?: string;
}): Promise<{
id: string;
name: string;
username: string;
description?: string;
followers_count?: number;
following_count?: number;
tweet_count?: number;
}> {
const bearerToken = getBearerToken(args.account ?? 'default');
if (!bearerToken) {
throw new Error('Missing Twitter credentials. Set TWITTER_{ACCOUNT}_BEARER_TOKEN');
}
const data = await twitterRequest(`/users/by/username/${args.username}`, bearerToken, {
'user.fields': 'description,public_metrics',
});
const user = data.data as { id: string; name: string; username: string; description?: string; public_metrics?: { followers_count?: number; following_count?: number; tweet_count?: number } };
return {
id: user.id,
name: user.name,
username: user.username,
description: user.description,
followers_count: user.public_metrics?.followers_count,
following_count: user.public_metrics?.following_count,
tweet_count: user.public_metrics?.tweet_count,
};
}
export async function getUserTweets(args: {
username: string;
max_results?: number;
account?: string;
}): Promise<Array<{
id: string;
text: string;
created_at?: string;
}>> {
const bearerToken = getBearerToken(args.account ?? 'default');
if (!bearerToken) {
throw new Error('Missing Twitter credentials. Set TWITTER_{ACCOUNT}_BEARER_TOKEN');
}
// First get user ID
const userData = await twitterRequest(`/users/by/username/${args.username}`, bearerToken);
const userId = userData.data?.id;
if (!userId) {
throw new Error(`User @${args.username} not found`);
}
const data = await twitterRequest(`/users/${userId}/tweets`, bearerToken, {
max_results: String(Math.min(args.max_results ?? 10, 100)),
'tweet.fields': 'created_at',
});
return (data.data ?? []) as Array<{ id: string; text: string; created_at?: string }>;
}
export async function createTweet(args: {
text: string;
account?: string;
}): Promise<{ message: string }> {
const bearerToken = getBearerToken(args.account ?? 'default');
if (!bearerToken) {
throw new Error('Missing Twitter credentials. Set TWITTER_{ACCOUNT}_BEARER_TOKEN');
}
throw new Error(
'Twitter/X posting requires a paid API tier. ' +
'The free tier is read-only (500 tweets/month). ' +
'Upgrade to Basic ($100/month) or Pro ($5,000/month) at https://developer.twitter.com/en/portal/products'
);
}