--- name: x description: Unified X (Twitter) CLI — fetch follows, diff snapshots, get likes/bookmarks, fetch latest posts, and take screenshots. Uses the official X API v2 with Bearer Token and OAuth 2.0 user-context auth. --- # X CLI — Unified X (Twitter) Tool Use the `xapi` CLI tool to interact with X (Twitter) via the official API v2. No cookies or scraping required. Backward-compatible alias: `twitter` also points to the same CLI. ## Setup ### Bearer Token (required for most commands) Get one from the [X Developer Portal](https://developer.x.com/en/portal/dashboard): 1. Create or select a project/app 2. Under "Keys and Tokens", find your Bearer Token 3. Set the environment variable: ```bash export X_BEARER_TOKEN='your_token_here' ``` ### OAuth 2.0 User Access Token (required for likes/bookmarks) The likes and bookmarks endpoints require an OAuth 2.0 user access token — app-only Bearer Tokens are not supported for these. 1. In the [Developer Portal](https://developer.x.com/en/portal/dashboard), go to your app's **Settings → User authentication settings → Set up** 2. Enable OAuth 2.0 and select **Web App** (confidential client) 3. Set a callback URL (e.g. `http://localhost:3000/callback`) 4. Complete the [OAuth 2.0 PKCE flow](https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code) to get an access token 5. Request scopes: `like.read`, `bookmark.read`, `tweet.read`, `users.read`, `offline.access` 6. Set the environment variables: ```bash export X_ACCESS_TOKEN='your_user_access_token_here' export X_REFRESH_TOKEN='your_refresh_token_here' # optional, for future token refresh support ``` ## Commands | Command | Description | |---------|-------------| | `xapi resolve ` | Resolve username to user ID + profile info | | `xapi fetch ` | Fetch and snapshot the full following list | | `xapi diff ` | Compare two snapshots for follows/unfollows | | `xapi latest-post ` | Fetch most recent original post for an account | | `xapi likes [username]` | Fetch liked tweets (default: HamelHusain, requires `X_ACCESS_TOKEN`) | | `xapi bookmarks [username]` | Fetch bookmarked tweets (default: HamelHusain, requires `X_ACCESS_TOKEN`) | | `xapi screenshot ` | Take a screenshot of a tweet (requires Playwright) | ## Usage ### Resolve a Username ```bash xapi resolve elonmusk ``` ### Fetch a Following Snapshot ```bash # Auto-saves to ~/.x/_.json xapi fetch elonmusk # Save to a specific file xapi fetch elonmusk -o /tmp/elon_following.json # Verbose mode shows progress xapi fetch elonmusk -v ``` ### Diff Two Snapshots Compare two snapshots to detect new follows and unfollows: ```bash # Basic diff xapi diff snapshot_before.json snapshot_after.json # Save diff output to file xapi diff before.json after.json -o diff_result.json # Also fetch the latest post for each unfollowed account # (uses the newer snapshot's timestamp as cutoff) xapi diff before.json after.json --fetch-posts -v ``` ### Fetch Latest Post ```bash xapi latest-post someuser # Get the latest post before a specific date xapi latest-post someuser --before 2026-01-15T00:00:00Z # Get the latest post before a specific post ID xapi latest-post someuser --before-post 1234567890 # Save to file xapi latest-post someuser -o latest.json ``` ### Fetch Likes ```bash # Fetch liked tweets for the default user (HamelHusain) xapi likes # Fetch for a different user xapi likes someuser # Limit to 20 most recent likes xapi likes --limit 20 # Save to file xapi likes -o likes.json ``` ### Fetch Bookmarks ```bash # Fetch bookmarked tweets for the default user (HamelHusain) xapi bookmarks # Fetch for a different user (must be your own account) xapi bookmarks yourusername # Limit and save xapi bookmarks --limit 50 -o bookmarks.json ``` ### Screenshot a Tweet ```bash # Screenshot a tweet (saves to tweet_.png) xapi screenshot https://x.com/user/status/1234567890 # Save to specific file xapi screenshot https://x.com/user/status/1234567890 -o my_tweet.png # Full page screenshot xapi screenshot https://x.com/user/status/1234567890 --full-page # Wait longer for slow loading tweets xapi screenshot https://x.com/user/status/1234567890 --wait 5 -v ``` ## Options | Option | Short | Description | |--------|-------|-------------| | `--output` | `-o` | Output file path (default: stdout or auto-generated) | | `--dir` | `-d` | Snapshot directory for fetch (default: `~/.x/`) | | `--verbose` | `-v` | Show progress information | | `--fetch-posts` | `-p` | Fetch latest post for unfollowed accounts (diff only) | | `--before` | | ISO 8601 datetime cutoff for latest-post | | `--before-post` | | Post ID cutoff for latest-post (returns posts older than this ID) | | `--limit` | `-n` | Maximum number of tweets to fetch (likes/bookmarks) | | `--full-page` | `-f` | Capture full page (screenshot only) | | `--wait` | `-w` | Seconds to wait for page load (screenshot only) | ## Output Formats ### Snapshot (fetch) ```json { "watched_username": "elonmusk", "watched_user_id": "44196397", "fetch_timestamp": "2026-01-01T00:00:00+00:00", "following_count": 500, "following": [ { "id": "12345", "username": "someuser", "name": "Some User", "description": "Bio text", "public_metrics": { "followers_count": 1000, "following_count": 200, "tweet_count": 5000 } } ] } ``` ### Diff Output ```json { "snapshot_before": { "file": "...", "watched_username": "...", "fetch_timestamp": "...", "following_count": 500 }, "snapshot_after": { "file": "...", "watched_username": "...", "fetch_timestamp": "...", "following_count": 502 }, "new_follows_count": 3, "unfollows_count": 1, "new_follows": [ { "id": "...", "username": "...", "name": "..." } ], "unfollows": [ { "id": "...", "username": "...", "name": "...", "latest_post": { "post_id": "...", "text": "...", "url": "..." } } ] } ``` ### Likes / Bookmarks Output ```json [ { "id": "1234567890", "text": "Tweet content here...", "created_at": "2026-01-01T00:00:00Z", "author": { "id": "987654321", "username": "example_user", "name": "Example User" }, "public_metrics": { "like_count": 100, "retweet_count": 10 }, "url": "https://x.com/example_user/status/1234567890" } ] ``` ## Environment Variables - `X_BEARER_TOKEN` — X API Bearer Token (required for resolve, fetch, diff, latest-post; also used by likes/bookmarks to resolve usernames) - `X_ACCESS_TOKEN` — OAuth 2.0 user access token (required for likes and bookmarks) - `X_REFRESH_TOKEN` — OAuth 2.0 refresh token (optional, for future token refresh support) - `X_DEFAULT_USERNAME` — Override the default username for likes/bookmarks (optional, default: `HamelHusain`) If a required variable is missing, the CLI exits with a clear error explaining what is needed and how to set it up. ## Default Username The `likes` and `bookmarks` commands default to `HamelHusain` if no username is provided. **Three ways to override (pick one):** 1. **Environment variable (recommended for skill installers):** Set `X_DEFAULT_USERNAME` to change the default for all invocations without editing code: ```bash export X_DEFAULT_USERNAME='yourusername' ``` Add this to your `~/.bashrc` or `~/.zshrc` to persist it. 2. **Per-invocation:** Pass a username argument to override just that call: ```bash xapi likes otherusername xapi bookmarks otherusername ``` 3. **Edit source code:** Change `_FALLBACK_USERNAME` at the top of `hamel/x_cli.py`: ```python _FALLBACK_USERNAME = "YourUsername" ``` ## Requirements The `hamel` package must be installed: `pip install hamel` **For `xapi screenshot`**: Playwright is included as a dependency, but you need to install browser binaries: ```bash playwright install chromium ``` ## Verify Setup After installing and setting the environment variables, run these commands to confirm everything works: ```bash # 1. Check the CLI is installed xapi --help # 2. Test API access (lightweight call — uses X_BEARER_TOKEN) xapi resolve github # 3. Test latest post xapi latest-post someuser # 4. Test likes (requires X_ACCESS_TOKEN, defaults to HamelHusain) xapi likes --limit 5 # 5. Test bookmarks (requires X_ACCESS_TOKEN, defaults to HamelHusain) xapi bookmarks --limit 5 ``` If `xapi resolve github` returns a JSON object with `"username": "github"`, your Bearer Token is working. If `xapi likes` returns JSON, your OAuth 2.0 user access token is working. ## Known Limitations - **Likes and bookmarks require `X_ACCESS_TOKEN`.** These endpoints use OAuth 2.0 user-context auth and do not work with `X_BEARER_TOKEN` alone. You need to complete the [OAuth 2.0 PKCE flow](https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code) and set `X_ACCESS_TOKEN`. Required scopes: `like.read` (likes), `bookmark.read` (bookmarks), `tweet.read`, `users.read`. - **Timeline window.** The user timeline endpoint only returns posts from approximately the last 7 days with app-only Bearer Token auth. Using `--before` with older dates will return no results. - **Protected accounts.** App-only auth cannot access following lists or tweets for protected accounts. ## Rate Limits The X API has rate limits per 15-minute window. The tool automatically handles rate limiting by waiting and retrying. For large following lists (>15k), fetches may take several minutes due to pagination and rate limits. ## Troubleshooting **"X_BEARER_TOKEN environment variable is not set"**: Get a Bearer Token from the [X Developer Portal](https://developer.x.com/en/portal/dashboard) and export it. **"API error 401"**: Your Bearer Token may be invalid or expired. Regenerate it in the Developer Portal. **"X_ACCESS_TOKEN environment variable is not set"**: Complete the OAuth 2.0 PKCE flow in the Developer Portal and set the token. See Setup section above. **"API error 403"**: For likes/bookmarks, ensure `X_ACCESS_TOKEN` is set (app-only Bearer Tokens are not supported). For other endpoints, check your app's access level in the Developer Portal. **"API error 429"**: Rate limited. The tool waits automatically, but if it persists, wait 15 minutes. **Empty following list**: The account may be protected. App-only auth cannot access protected accounts' following lists. **playwright not installed**: For screenshots, install with `pip install playwright && playwright install chromium`. ## Examples **Monitor follows over time:** ```bash xapi fetch someuser -o ~/snapshots/day1.json # ... later ... xapi fetch someuser -o ~/snapshots/day2.json xapi diff ~/snapshots/day1.json ~/snapshots/day2.json --fetch-posts -v ``` **Download likes and analyze with AI:** ```bash xapi likes -o /tmp/likes.json ai-gem "What topics am I most interested in based on my likes?" /tmp/likes.json ``` **Export bookmarks:** ```bash xapi bookmarks -o ~/bookmarks-backup.json ``` **Get the last post before a specific tweet:** ```bash xapi latest-post someuser --before-post 1234567890 ```