# bird 🐦 β€” fast X CLI for tweeting, replying, and reading `bird` is a fast X CLI for tweeting, replying, and reading via X/Twitter GraphQL through twitter safe relay. ## Disclaimer This project uses X/Twitter’s **undocumented** web GraphQL API through twitter safe relay. X can change endpoints, query IDs, and anti-bot behavior at any time β€” **expect this to break without notice**. ## Install ```bash npm install -g @yuta/bird # or pnpm add -g @yuta/bird # or bun add -g @yuta/bird # one-shot (no install) bunx @yuta/bird whoami ``` Gitea Packages: ```bash npm config set @yuta:registry=https://git.yutakobayashi.com/api/packages/yuta/npm/ npm install -g @yuta/bird ``` Nix flakes: ```bash nix run git+https://git.yutakobayashi.com/yuta/bird -- --help ``` Homebrew (macOS, prebuilt Bun binary): ```bash brew install steipete/tap/bird ``` ## Quickstart ```bash # Point bird at your running twitter safe relay export TWITTER_RELAY_BASE_URL=http://localhost:6900 # Show the logged-in account bird whoami # Discover command help bird help whoami # Read a tweet (URL or ID) bird read https://x.com/user/status/1234567890123456789 bird 1234567890123456789 --json # Thread + replies bird thread https://x.com/user/status/1234567890123456789 bird replies 1234567890123456789 bird replies 1234567890123456789 --max-pages 3 --json bird thread 1234567890123456789 --max-pages 3 --json # Search + mentions bird search "from:steipete" -n 5 bird mentions -n 5 bird mentions --user @steipete -n 5 # User tweets (profile timeline) bird user-tweets @steipete -n 20 bird user-tweets @steipete -n 50 --json # Bookmarks bird bookmarks -n 5 bird bookmarks --folder-id 123456789123456789 -n 5 # https://x.com/i/bookmarks/ bird bookmarks --all --json bird bookmarks --all --max-pages 2 --json bird unbookmark 1234567890123456789 bird unbookmark https://x.com/user/status/1234567890123456789 # Likes bird likes -n 5 # News and trending topics (AI-curated from Explore tabs) bird news --ai-only -n 10 bird news --sports -n 5 # Lists bird list-timeline 1234567890 -n 20 bird list-timeline https://x.com/i/lists/1234567890 --all --json bird list-timeline 1234567890 --max-pages 3 --json # Following (who you follow) bird following -n 20 bird following --user 12345678 -n 10 # by user ID # Followers (who follows you) bird followers -n 20 bird followers --user 12345678 -n 10 # by user ID # Refresh GraphQL query IDs cache (no rebuild) bird query-ids --fresh ``` ## News & Trending Fetch AI-curated news and trending topics from X's Explore page tabs: ```bash # Fetch 10 news items from all tabs (default: For You, News, Sports, Entertainment) bird news -n 10 # Fetch only AI-curated news (filters out regular trends) bird news --ai-only -n 20 # Fetch from specific tabs bird news --news-only --ai-only -n 10 bird news --sports -n 15 bird news --entertainment --ai-only -n 5 # Include related tweets for each news item bird news --with-tweets --tweets-per-item 3 -n 10 # Combine multiple tab filters bird news --sports --entertainment -n 20 # JSON output bird news --json -n 5 bird news --json-full --ai-only -n 10 # includes raw API response ``` Tab options (can be combined): - `--for-you` β€” Fetch from For You tab only - `--news-only` β€” Fetch from News tab only - `--sports` β€” Fetch from Sports tab only - `--entertainment` β€” Fetch from Entertainment tab only - `--trending-only` β€” Fetch from Trending tab only By default, the command fetches from For You, News, Sports, and Entertainment tabs (Trending excluded to reduce noise). Headlines are automatically deduplicated across tabs. ## Library `bird` can be used as a library (same GraphQL client as the CLI): ```ts import { TwitterClient } from '@yuta/bird'; const client = new TwitterClient({ relayBaseUrl: process.env.TWITTER_RELAY_BASE_URL }); // Search for tweets const searchResult = await client.search('from:steipete', 50); // Fetch news and trending topics from all tabs (default: For You, News, Sports, Entertainment) const newsResult = await client.getNews(10, { aiOnly: true }); // Fetch from specific tabs with related tweets const sportsNews = await client.getNews(10, { aiOnly: true, withTweets: true, tabs: ['sports', 'entertainment'] }); ``` ## Commands - `bird tweet ""` β€” post a new tweet. - `bird reply ""` β€” reply to a tweet using its ID or URL. - `bird help [command]` β€” show help (or help for a subcommand). - `bird query-ids [--fresh] [--json]` β€” inspect or refresh cached GraphQL query IDs. - `bird read [--json]` β€” fetch tweet content as text or JSON. - `bird [--json]` β€” shorthand for `read` when only a URL or ID is provided. - `bird replies [--all] [--max-pages n] [--cursor string] [--delay ms] [--json]` β€” list replies to a tweet. - `bird thread [--all] [--max-pages n] [--cursor string] [--delay ms] [--json]` β€” show the full conversation thread. - `bird search "" [-n count] [--all] [--max-pages n] [--cursor string] [--json]` β€” search for tweets matching a query; `--max-pages` requires `--all` or `--cursor`. - `bird mentions [-n count] [--user @handle] [--json]` β€” find tweets mentioning a user (defaults to the authenticated user). - `bird user-tweets <@handle> [-n count] [--cursor string] [--max-pages n] [--delay ms] [--json]` β€” get tweets from a user's profile timeline. - `bird bookmarks [-n count] [--folder-id id] [--all] [--max-pages n] [--json]` β€” list your bookmarked tweets (or a specific bookmark folder); `--max-pages` requires `--all`. - `bird unbookmark ` β€” remove one or more bookmarks by tweet ID or URL. - `bird likes [-n count] [--json]` β€” list your liked tweets. - `bird news [-n count] [--ai-only] [--with-tweets] [--tweets-per-item n] [--for-you] [--news-only] [--sports] [--entertainment] [--trending-only] [--json]` β€” fetch news and trending topics from X's Explore tabs. - `bird trending` β€” alias for `news` command. - `bird list-timeline [-n count] [--all] [--max-pages n] [--cursor string] [--json]` β€” get tweets from a list timeline; `--max-pages` implies `--all`. - `bird following [--user ] [-n count] [--json]` β€” list users that you (or another user) follow. - `bird followers [--user ] [-n count] [--json]` β€” list users that follow you (or another user). - `bird whoami` β€” print which Twitter account your relay belongs to. - `bird check` β€” verify relay configuration and account access. - `bird likes [-n count] [--json]` β€” list your liked tweets. - `bird news [-n count] [--ai-only] [--with-tweets] [--tweets-per-item n] [--for-you] [--news-only] [--sports] [--entertainment] [--trending-only] [--json]` β€” fetch news and trending topics from X's Explore tabs (fetches from For You, News, Sports, and Entertainment tabs by default). - `bird trending` β€” alias for `news` command. - `bird list-timeline [-n count] [--all] [--max-pages n] [--cursor string] [--json]` β€” get tweets from a list timeline; `--max-pages` implies `--all`. - `bird following [--user ] [-n count] [--json]` β€” list users that you (or another user) follow. - `bird followers [--user ] [-n count] [--json]` β€” list users that follow you (or another user). - `bird whoami` β€” print which Twitter account your relay belongs to. - `bird check` β€” verify relay configuration and account access. Global options: - `--relay-base-url `: twitter safe relay base URL (defaults to `TWITTER_RELAY_BASE_URL`). - `--timeout `: abort requests after the given timeout (milliseconds). - `--quote-depth `: max quoted tweet depth in JSON output (default: 1; 0 disables). - `--plain`: stable output (no emoji, no color). - `--no-emoji`: disable emoji output. - `--no-color`: disable ANSI colors (or set `NO_COLOR=1`). - `--media `: attach media file (repeatable, up to 4 images or 1 video). - `--alt `: alt text for the corresponding `--media` (repeatable). ## Authentication (GraphQL) `bird` does not handle Twitter authentication locally. It sends Twitter API-compatible requests to twitter safe relay, and the relay handles authentication. Write operations: - `tweet`/`reply` primarily use GraphQL (`CreateTweet`). - If GraphQL returns error `226` (β€œautomated request”), `bird` falls back to the legacy `statuses/update.json` endpoint. Configure the relay URL with `TWITTER_RELAY_BASE_URL` or `--relay-base-url`. ## Config (JSON5) Config precedence: CLI flags > env vars > project config > global config. - Global: `~/.config/bird/config.json5` - Project: `./.birdrc.json5` Example `~/.config/bird/config.json5`: ```json5 { relayBaseUrl: "http://localhost:6900", timeoutMs: 20000, quoteDepth: 1 } ``` Environment shortcuts: - `TWITTER_RELAY_BASE_URL` - `BIRD_TIMEOUT_MS` - `BIRD_QUOTE_DEPTH` ## Output - `--json` prints raw tweet objects for read/replies/thread/search/mentions/user-tweets/bookmarks/likes. - When using `--json` with pagination (`--all`, `--cursor`, `--max-pages`, or for `user-tweets` when `-n > 20`), output is `{ tweets, nextCursor }`. - `read` returns full text for Notes and Articles when present. - Use `--plain` for stable, script-friendly output (no emoji, no color). ### JSON Schema When using `--json`, tweet objects include: | Field | Type | Description | |-------|------|-------------| | `id` | string | Tweet ID | | `text` | string | Full tweet text (includes Note/Article content when present) | | `author` | object | `{ username, name }` | | `authorId` | string? | Author's user ID | | `createdAt` | string | Timestamp | | `replyCount` | number | Number of replies | | `retweetCount` | number | Number of retweets | | `likeCount` | number | Number of likes | | `conversationId` | string | Thread conversation ID | | `inReplyToStatusId` | string? | Parent tweet ID (present if this is a reply) | | `quotedTweet` | object? | Embedded quote tweet (same schema; depth controlled by `--quote-depth`) | When using `--json` with `following`/`followers`, user objects include: | Field | Type | Description | |-------|------|-------------| | `id` | string | User ID | | `username` | string | Username/handle | | `name` | string | Display name | | `description` | string? | User bio | | `followersCount` | number? | Followers count | | `followingCount` | number? | Following count | | `isBlueVerified` | boolean? | Blue verified flag | | `profileImageUrl` | string? | Profile image URL | | `createdAt` | string? | Account creation timestamp | When using `--json` with `news`/`trending`, news objects include: | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique identifier for the news item | | `headline` | string | News headline or trend title | | `category` | string? | Category (e.g., "AI Β· Technology", "Trending", "News") | | `timeAgo` | string? | Relative time (e.g., "2h ago") | | `postCount` | number? | Number of posts | | `description` | string? | Item description | | `url` | string? | URL to the trend or news article | | `tweets` | array? | Related tweets (only when `--with-tweets` is used) | | `_raw` | object? | Raw API response (only when `--json-full` is used) | ## Query IDs (GraphQL) X rotates GraphQL β€œquery IDs” frequently. Each GraphQL operation is addressed as: - `operationName` (e.g. `TweetDetail`, `CreateTweet`) - `queryId` (rotating ID baked into X’s web client bundles) `bird` ships with a baseline mapping in `src/lib/query-ids.json` (copied into `dist/` on build). At runtime, it can refresh that mapping by scraping X’s public web client bundles and caching the result on disk. Runtime cache: - Default path: `~/.config/bird/query-ids-cache.json` - Override path: `BIRD_QUERY_IDS_CACHE=/path/to/file.json` - TTL: 24h (stale cache is still used, but marked β€œnot fresh”) Auto-recovery: - On GraphQL `404` (query ID invalid), `bird` forces a refresh once and retries. - For `TweetDetail`/`SearchTimeline`, `bird` also rotates through a small set of known fallback IDs to reduce breakage while refreshing. Refresh on demand: ```bash bird query-ids --fresh ``` Exit codes: - `0`: success - `1`: runtime error (network/auth/etc) - `2`: invalid usage/validation (e.g. bad `--user` handle) ## Version `bird --version` prints `package.json` version plus current git sha when available, e.g. `0.3.0 (3df7969b)`. ## Media uploads - Attach media with `--media` (repeatable) and optional `--alt` per item. - Up to 4 images/GIFs, or 1 video (no mixing). Supported: jpg, jpeg, png, webp, gif, mp4, mov. - Images/GIFs + 1 video supported (uploads via Twitter legacy upload endpoint through the relay; video may take longer to process). Example: ```bash bird tweet "hi" --media img.png --alt "desc" ``` ## Development ```bash cd ~/Projects/bird pnpm install pnpm run build # dist/ + bun binary pnpm run build:dist # dist/ only pnpm run build:binary nix build .#bird nix run .# -- --help pnpm run dev tweet "Test" pnpm run dev -- --plain check pnpm test pnpm run lint ``` ## Notes - GraphQL uses internal X endpoints and can be rate limited (429). - Query IDs rotate; refresh at runtime with `bird query-ids --fresh` (or update the baked baseline via `pnpm run graphql:update`).