Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
Bird
https://github.com/steipete/bird
Admin
Bird is a fast command-line interface (CLI) for tweeting, replying, and reading on X/Twitter using
...
Tokens:
9,915
Snippets:
71
Trust Score:
10
Update:
4 months ago
Context
Skills
Chat
Benchmark
79.1
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# bird 🐦 — Fast X CLI for Tweeting, Replying, and Reading bird is a CLI tool and TypeScript library for interacting with X/Twitter through its undocumented GraphQL API. It provides fast, authenticated access to tweet, reply, read, search, and retrieve bookmarks, likes, followers, and following lists using cookie-based authentication. The tool extracts session cookies from Safari, Chrome, or Firefox browsers, eliminating the need for API keys or OAuth flows. The library is built for both command-line usage and programmatic integration. It handles X's frequently rotating GraphQL query IDs through automatic runtime refreshes, provides fallback mechanisms for API changes, supports media uploads (images, GIFs, videos), and includes comprehensive error handling with automatic recovery strategies. The tool operates entirely through X's web client endpoints, authenticating via `auth_token` and `ct0` cookies. ## Library Usage ### Initialize Client with Browser Cookies ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; // Auto-detect cookies from Safari (default priority: safari, chrome, firefox) const { cookies, warnings } = await resolveCredentials({ cookieSource: 'safari' }); // Display any warnings about cookie extraction for (const warning of warnings) { console.warn(warning); } // Verify credentials are available if (!cookies.authToken || !cookies.ct0) { throw new Error('Missing required credentials. Please log in to x.com in your browser.'); } // Create client instance const client = new TwitterClient({ cookies, timeoutMs: 20000, // Request timeout (optional) quoteDepth: 1 // Max depth for quoted tweets (default: 1) }); console.log(`Authenticated via: ${cookies.source}`); ``` ### Post a Tweet ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); // Post a simple text tweet const result = await client.tweet('Hello from bird! 🐦'); if (result.success) { console.log(`Tweet posted: https://x.com/i/status/${result.tweetId}`); } else { console.error(`Failed to post tweet: ${result.error}`); } ``` ### Post a Tweet with Media ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; import { readFileSync } from 'node:fs'; const { cookies } = await resolveCredentials({ cookieSource: 'chrome' }); const client = new TwitterClient({ cookies }); // Upload media first const imageBuffer = readFileSync('./photo.jpg'); const uploadResult = await client.uploadMedia({ data: imageBuffer, mimeType: 'image/jpeg', alt: 'A beautiful sunset over the mountains' }); if (!uploadResult.success || !uploadResult.mediaId) { throw new Error(`Media upload failed: ${uploadResult.error}`); } // Post tweet with attached media const result = await client.tweet( 'Check out this amazing view! 🌄', [uploadResult.mediaId] ); if (result.success) { console.log(`Tweet with media posted: https://x.com/i/status/${result.tweetId}`); } else { console.error(`Failed to post tweet: ${result.error}`); } ``` ### Reply to a Tweet ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'firefox' }); const client = new TwitterClient({ cookies }); const tweetId = '1234567890123456789'; const replyText = 'Great point! I completely agree. 👍'; const result = await client.reply(replyText, tweetId); if (result.success) { console.log(`Reply posted: https://x.com/i/status/${result.tweetId}`); } else { console.error(`Failed to post reply: ${result.error}`); } ``` ### Read a Tweet ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies, quoteDepth: 2 }); const tweetId = '1234567890123456789'; const result = await client.getTweet(tweetId); if (result.success && result.tweet) { const tweet = result.tweet; console.log(`@${tweet.author.username} (${tweet.author.name}):`); console.log(tweet.text); console.log(`\nStats: ${tweet.likeCount} likes, ${tweet.retweetCount} retweets, ${tweet.replyCount} replies`); console.log(`Posted: ${tweet.createdAt}`); // Check for quoted tweet if (tweet.quotedTweet) { console.log(`\nQuoted: @${tweet.quotedTweet.author.username}: ${tweet.quotedTweet.text}`); } } else { console.error(`Failed to fetch tweet: ${result.error}`); } ``` ### Search Tweets ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); // Search for tweets from a specific user const searchResult = await client.search('from:steipete', 50); if (searchResult.success && searchResult.tweets) { console.log(`Found ${searchResult.tweets.length} tweets`); for (const tweet of searchResult.tweets) { console.log(`\n@${tweet.author.username}: ${tweet.text}`); console.log(` ${tweet.likeCount} ❤️ ${tweet.retweetCount} 🔁 ${tweet.replyCount} 💬`); } } else { console.error(`Search failed: ${searchResult.error}`); } // Advanced search queries const queries = [ '@username', // Mentions 'from:username', // Tweets from user 'to:username', // Replies to user 'keyword1 keyword2', // Multiple keywords '"exact phrase"', // Exact match 'keyword -excluded', // Exclude terms 'keyword since:2024-01-01', // Date range 'keyword filter:media', // Only tweets with media 'keyword filter:replies', // Only replies ]; ``` ### Get Thread Conversation ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); const tweetId = '1234567890123456789'; const result = await client.getThread(tweetId); if (result.success && result.tweets) { console.log(`Thread has ${result.tweets.length} tweets:\n`); for (const tweet of result.tweets) { console.log(`@${tweet.author.username}: ${tweet.text}`); if (tweet.inReplyToStatusId) { console.log(` ↳ Replying to tweet ${tweet.inReplyToStatusId}`); } console.log('─'.repeat(50)); } } else { console.error(`Failed to fetch thread: ${result.error}`); } ``` ### Get Replies to a Tweet ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); const tweetId = '1234567890123456789'; const result = await client.getReplies(tweetId); if (result.success && result.tweets) { console.log(`Found ${result.tweets.length} direct replies:\n`); for (const reply of result.tweets) { console.log(`@${reply.author.username} replied:`); console.log(reply.text); console.log(` ${reply.likeCount} likes\n`); } } else { console.error(`Failed to fetch replies: ${result.error}`); } ``` ### Get Bookmarks ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); // Get all bookmarks const result = await client.getBookmarks(20); if (result.success && result.tweets) { console.log(`Found ${result.tweets.length} bookmarked tweets:\n`); for (const tweet of result.tweets) { console.log(`@${tweet.author.username}: ${tweet.text.substring(0, 100)}...`); console.log(` URL: https://x.com/${tweet.author.username}/status/${tweet.id}\n`); } } else { console.error(`Failed to fetch bookmarks: ${result.error}`); } // Get bookmarks from a specific folder // Folder ID from URL: https://x.com/i/bookmarks/<folder-id> const folderId = '123456789123456789'; const folderResult = await client.getBookmarkFolderTimeline(folderId, 10); if (folderResult.success && folderResult.tweets) { console.log(`Folder has ${folderResult.tweets.length} bookmarks`); } ``` ### Get Liked Tweets ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); const result = await client.getLikes(20); if (result.success && result.tweets) { console.log(`Found ${result.tweets.length} liked tweets:\n`); for (const tweet of result.tweets) { console.log(`@${tweet.author.username}: ${tweet.text}`); console.log(` Liked on: ${tweet.createdAt}\n`); } } else { console.error(`Failed to fetch likes: ${result.error}`); } ``` ### Get Following and Followers ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); // Get current user info first const currentUser = await client.getCurrentUser(); if (!currentUser.success || !currentUser.user) { throw new Error(`Failed to get current user: ${currentUser.error}`); } console.log(`Logged in as: @${currentUser.user.username} (${currentUser.user.name})`); console.log(`User ID: ${currentUser.user.id}\n`); // Get users you follow const followingResult = await client.getFollowing(currentUser.user.id, 20); if (followingResult.success && followingResult.users) { console.log(`You follow ${followingResult.users.length} users:\n`); for (const user of followingResult.users) { console.log(`@${user.username} - ${user.name}`); if (user.description) { console.log(` ${user.description.substring(0, 80)}...`); } console.log(` ${user.followersCount?.toLocaleString()} followers`); if (user.isBlueVerified) { console.log(` ✓ Verified`); } console.log(); } } // Get your followers const followersResult = await client.getFollowers(currentUser.user.id, 20); if (followersResult.success && followersResult.users) { console.log(`\nYou have ${followersResult.users.length} followers`); } // Get following/followers for another user const otherUserId = '44196397'; // @elonmusk const othersFollowing = await client.getFollowing(otherUserId, 10); ``` ### Credential Resolution with Environment Variables ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; // Set environment variables (alternative to browser cookies) process.env.AUTH_TOKEN = 'your_auth_token_here'; process.env.CT0 = 'your_ct0_token_here'; // Credentials will be resolved in this order: // 1. Explicit authToken/ct0 parameters // 2. Environment variables (AUTH_TOKEN, CT0 or TWITTER_AUTH_TOKEN, TWITTER_CT0) // 3. Browser cookies (safari, chrome, firefox) const { cookies, warnings } = await resolveCredentials({ cookieSource: ['firefox', 'safari', 'chrome'], // Priority order firefoxProfile: 'default-release', chromeProfile: 'Default', cookieTimeoutMs: 30000 }); if (warnings.length > 0) { console.warn('Credential warnings:', warnings); } const client = new TwitterClient({ cookies, timeoutMs: 20000 }); ``` ### Handle Query ID Rotation ```typescript import { TwitterClient, resolveCredentials, runtimeQueryIds } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies }); // Query IDs are automatically refreshed on 404 errors // Manual refresh if needed: await runtimeQueryIds.refresh(['TweetDetail', 'SearchTimeline'], { force: true }); // Get current query ID cache info const info = await runtimeQueryIds.getSnapshotInfo(); if (info) { console.log(`Query IDs cached at: ${info.cachePath}`); console.log(`Fetched: ${info.snapshot.fetchedAt}`); console.log(`Fresh: ${info.isFresh}`); console.log(`Age: ${Math.round(info.ageMs / 1000 / 60)} minutes`); console.log(`Operations: ${Object.keys(info.snapshot.ids).length}`); // Available operations: // CreateTweet, CreateRetweet, FavoriteTweet, TweetDetail, SearchTimeline, // UserArticlesTweets, Bookmarks, Following, Followers, Likes, BookmarkFolderTimeline } // Cache location: ~/.config/bird/query-ids-cache.json // Override with: BIRD_QUERY_IDS_CACHE=/custom/path.json // TTL: 24 hours (stale cache still used but marked not fresh) ``` ### Error Handling and Fallbacks ```typescript import { TwitterClient, resolveCredentials } from '@steipete/bird'; const { cookies } = await resolveCredentials({ cookieSource: 'safari' }); const client = new TwitterClient({ cookies, timeoutMs: 10000 // 10 second timeout }); // All methods return result objects with success flags const result = await client.tweet('Test post'); if (!result.success) { // Handle specific error cases if (result.error?.includes('automated request')) { console.error('Detected as automated. Fallback to legacy endpoint attempted.'); } else if (result.error?.includes('HTTP 429')) { console.error('Rate limited. Wait before retrying.'); } else if (result.error?.includes('HTTP 404')) { console.error('Query ID invalid. Automatic refresh attempted.'); } else if (result.error?.includes('timeout')) { console.error('Request timed out. Try increasing timeoutMs.'); } else { console.error(`Error: ${result.error}`); } process.exit(1); } // The library includes automatic fallbacks: // - GraphQL error 226 ("automated request") → legacy statuses/update.json endpoint // - GraphQL 404 → automatic query ID refresh + retry // - TweetDetail/SearchTimeline → rotate through known fallback query IDs ``` ## CLI Usage ### Post a Tweet ```bash # Simple tweet bird tweet "Hello from bird! 🐦" # Tweet with media attachments bird tweet "Check out these photos!" \ --media photo1.jpg \ --media photo2.png \ --alt "First photo description" \ --alt "Second photo description" # Tweet with video (max 1 video, no mixing with images) bird tweet "Watch this!" --media video.mp4 # Specify browser for cookie extraction bird --cookie-source firefox tweet "Using Firefox cookies" bird --firefox-profile default-release tweet "Using specific profile" ``` ### Reply to a Tweet ```bash # Reply using tweet ID bird reply 1234567890123456789 "Great point!" # Reply using full URL bird reply https://x.com/user/status/1234567890123456789 "Totally agree!" # Reply with media bird reply 1234567890123456789 "Here's my response" \ --media screenshot.png \ --alt "Screenshot showing the issue" ``` ### Read and Explore Tweets ```bash # Read a tweet (supports Notes and Articles) bird read https://x.com/user/status/1234567890123456789 # Shorthand syntax bird 1234567890123456789 # JSON output bird read 1234567890123456789 --json # Get replies to a tweet bird replies 1234567890123456789 bird replies https://x.com/user/status/1234567890123456789 --json # Get full conversation thread bird thread 1234567890123456789 bird thread https://x.com/user/status/1234567890123456789 --json ``` ### Search Tweets ```bash # Basic search bird search "from:steipete" -n 10 # Search mentions bird mentions -n 20 bird mentions --user @steipete -n 5 # Complex queries bird search "typescript OR javascript" -n 50 bird search '"exact phrase" since:2024-01-01' -n 30 --json ``` ### Get Bookmarks and Likes ```bash # Get your bookmarks bird bookmarks -n 20 # Get bookmarks from specific folder bird bookmarks --folder-id 123456789123456789 -n 10 # Get liked tweets bird likes -n 20 --json ``` ### Get Following and Followers ```bash # Get users you follow bird following -n 20 # Get users following you bird followers -n 20 # Get following/followers for specific user (by user ID) bird following --user 44196397 -n 10 bird followers --user 44196397 -n 10 --json ``` ### Authentication and Configuration ```bash # Check which account you're logged in as bird whoami # Check credential availability bird check # Use specific browser bird --cookie-source chrome whoami bird --chrome-profile "Profile 1" whoami # Use environment variables export AUTH_TOKEN="your_auth_token" export CT0="your_ct0_token" bird whoami # Use CLI flags (highest priority) bird --auth-token "..." --ct0 "..." whoami # Global options bird --timeout 30000 search "query" # 30 second timeout bird --quote-depth 2 read 1234567890 # Include nested quotes bird --plain search "query" # No emoji, no color bird --no-color --no-emoji read 1234567890 # Script-friendly output ``` ### Configuration File ```bash # Global config: ~/.config/bird/config.json5 # Project config: ./.birdrc.json5 cat > ~/.config/bird/config.json5 << 'EOF' { // Cookie source priority order cookieSource: ["firefox", "safari", "chrome"], // Browser profile settings firefoxProfile: "default-release", chromeProfile: "Default", // Timeout settings (milliseconds) cookieTimeoutMs: 30000, timeoutMs: 20000, // Max depth for quoted tweets in JSON output quoteDepth: 1 } EOF # Environment variable shortcuts export BIRD_TIMEOUT_MS=30000 export BIRD_COOKIE_TIMEOUT_MS=30000 export BIRD_QUOTE_DEPTH=2 ``` ### Query ID Management ```bash # Show cached GraphQL query IDs bird query-ids # Force refresh query IDs (scrapes X client bundles) bird query-ids --fresh # JSON output bird query-ids --fresh --json # Cache location and behavior: # - Default: ~/.config/bird/query-ids-cache.json # - TTL: 24 hours # - Auto-refresh on GraphQL 404 errors # - Override: BIRD_QUERY_IDS_CACHE=/custom/path.json ``` ## Integration Patterns and Use Cases bird is designed for both interactive CLI usage and programmatic integration into larger systems. For CLI automation, the `--json` flag outputs structured data that can be parsed by shell scripts, CI/CD pipelines, or monitoring systems. The `--plain` flag ensures stable, script-friendly output without color codes or emoji. Exit codes distinguish between runtime errors (1) and validation errors (2), enabling robust error handling in automated workflows. Configuration files support per-project settings, making it easy to manage multiple Twitter accounts or different authentication strategies across environments. For library integration, bird provides a clean TypeScript API with promise-based async methods and strongly-typed return values. All operations return result objects with explicit `success` boolean flags, making error handling straightforward and type-safe. The client handles authentication transparently, supporting multiple credential sources with automatic fallback (CLI flags → environment variables → browser cookies). Media uploads are streamlined with automatic chunking, progress monitoring for videos, and metadata handling for alt text. The library manages X's GraphQL API complexity internally, including query ID rotation, retry logic, and fallback endpoints, exposing a simple, stable interface that abstracts away platform-specific implementation details. Use cases include social media management tools, content scheduling systems, analytics dashboards, bot accounts, automated customer support, and integration with existing Node.js/TypeScript applications requiring programmatic Twitter access without OAuth complexity.