#!/usr/bin/env node /** * bird - CLI tool for posting tweets and replies * * Usage: * bird tweet "Hello world!" * bird reply "This is a reply" * bird reply "This is a reply" * bird read */ import { Command } from 'commander'; import { resolveCredentials } from './lib/cookies.js'; import { extractTweetId } from './lib/extract-tweet-id.js'; import { TwitterClient } from './lib/twitter-client.js'; const program = new Command(); program.name('bird').description('Post tweets and replies via Twitter/X GraphQL API').version('0.1.0'); // Global options for authentication program .option('--auth-token ', 'Twitter auth_token cookie') .option('--ct0 ', 'Twitter ct0 cookie') .option('--chrome-profile ', 'Chrome profile name for cookie extraction'); // Tweet command program .command('tweet') .description('Post a new tweet') .argument('', 'Tweet text') .action(async (text: string) => { const opts = program.opts(); const { cookies, warnings } = await resolveCredentials({ authToken: opts.authToken, ct0: opts.ct0, chromeProfile: opts.chromeProfile, }); for (const warning of warnings) { console.error(`āš ļø ${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error('āŒ Missing required credentials'); process.exit(1); } if (cookies.source) { console.error(`šŸ“ Using credentials from: ${cookies.source}`); } const client = new TwitterClient({ cookies }); const result = await client.tweet(text); if (result.success) { console.log('āœ… Tweet posted successfully!'); console.log(`šŸ”— https://x.com/i/status/${result.tweetId}`); } else { console.error(`āŒ Failed to post tweet: ${result.error}`); process.exit(1); } }); // Reply command program .command('reply') .description('Reply to an existing tweet') .argument('', 'Tweet ID or URL to reply to') .argument('', 'Reply text') .action(async (tweetIdOrUrl: string, text: string) => { const opts = program.opts(); const { cookies, warnings } = await resolveCredentials({ authToken: opts.authToken, ct0: opts.ct0, chromeProfile: opts.chromeProfile, }); for (const warning of warnings) { console.error(`āš ļø ${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error('āŒ Missing required credentials'); process.exit(1); } if (cookies.source) { console.error(`šŸ“ Using credentials from: ${cookies.source}`); } const tweetId = extractTweetId(tweetIdOrUrl); console.error(`šŸ“ Replying to tweet: ${tweetId}`); const client = new TwitterClient({ cookies }); const result = await client.reply(text, tweetId); if (result.success) { console.log('āœ… Reply posted successfully!'); console.log(`šŸ”— https://x.com/i/status/${result.tweetId}`); } else { console.error(`āŒ Failed to post reply: ${result.error}`); process.exit(1); } }); // Read command - fetch tweet content program .command('read') .description('Read/fetch a tweet by ID or URL') .argument('', 'Tweet ID or URL to read') .option('--json', 'Output as JSON') .action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => { const opts = program.opts(); const { cookies, warnings } = await resolveCredentials({ authToken: opts.authToken, ct0: opts.ct0, chromeProfile: opts.chromeProfile, }); for (const warning of warnings) { console.error(`āš ļø ${warning}`); } if (!cookies.authToken || !cookies.ct0) { console.error('āŒ Missing required credentials'); process.exit(1); } const tweetId = extractTweetId(tweetIdOrUrl); const client = new TwitterClient({ cookies }); const result = await client.getTweet(tweetId); if (result.success && result.tweet) { if (cmdOpts.json) { console.log(JSON.stringify(result.tweet, null, 2)); } else { console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`); console.log(result.tweet.text); if (result.tweet.createdAt) { console.log(`\nšŸ“… ${result.tweet.createdAt}`); } console.log( `ā¤ļø ${result.tweet.likeCount ?? 0} šŸ” ${result.tweet.retweetCount ?? 0} šŸ’¬ ${result.tweet.replyCount ?? 0}`, ); } } else { console.error(`āŒ Failed to read tweet: ${result.error}`); process.exit(1); } }); // Check command - verify credentials program .command('check') .description('Check credential availability') .action(async () => { const opts = program.opts(); const { cookies, warnings } = await resolveCredentials({ authToken: opts.authToken, ct0: opts.ct0, chromeProfile: opts.chromeProfile, }); console.log('šŸ” Credential Check'); console.log('─'.repeat(40)); if (cookies.authToken) { console.log(`āœ… auth_token: ${cookies.authToken.slice(0, 10)}...`); } else { console.log('āŒ auth_token: not found'); } if (cookies.ct0) { console.log(`āœ… ct0: ${cookies.ct0.slice(0, 10)}...`); } else { console.log('āŒ ct0: not found'); } if (cookies.source) { console.log(`šŸ“ Source: ${cookies.source}`); } if (warnings.length > 0) { console.log('\nāš ļø Warnings:'); for (const warning of warnings) { console.log(` - ${warning}`); } } if (cookies.authToken && cookies.ct0) { console.log('\nāœ… Ready to tweet!'); } else { console.log('\nāŒ Missing credentials. Options:'); console.log(' 1. Login to x.com in Chrome'); console.log(' 2. Set AUTH_TOKEN and CT0 environment variables'); console.log(' 3. Use --auth-token and --ct0 flags'); process.exit(1); } }); program.parse();