Initial commit: twitter-cli for posting tweets via GraphQL API

Features:
- Post tweets and replies via Twitter GraphQL API
- Cookie extraction from Chrome (macOS)
- Env var and CLI argument authentication
- 19 passing tests with vitest
- Biome for linting
This commit is contained in:
Peter Steinberger
2025-11-28 06:35:17 +00:00
parent e9f34afab0
commit 011476f607
14 changed files with 2411 additions and 0 deletions
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env node
/**
* twitter-cli - CLI tool for posting tweets and replies
*
* Usage:
* twitter-cli tweet "Hello world!"
* twitter-cli reply <tweet-id> "This is a reply"
* twitter-cli reply <tweet-url> "This is a reply"
*/
import { Command } from 'commander';
import { resolveCredentials } from './lib/cookies.js';
import { TwitterClient } from './lib/twitter-client.js';
const program = new Command();
program.name('twitter-cli').description('Post tweets and replies via Twitter GraphQL API').version('0.1.0');
// Global options for authentication
program
.option('--auth-token <token>', 'Twitter auth_token cookie')
.option('--ct0 <token>', 'Twitter ct0 cookie')
.option('--chrome-profile <name>', 'Chrome profile name for cookie extraction');
/**
* Extract tweet ID from URL or return as-is if already an ID
*/
function extractTweetId(input: string): string {
// If it's a URL, extract the tweet ID
const urlMatch = input.match(/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/);
if (urlMatch) {
return urlMatch[1];
}
// Assume it's already an ID
return input;
}
// Tweet command
program
.command('tweet')
.description('Post a new tweet')
.argument('<text>', '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>', 'Tweet ID or URL to reply to')
.argument('<text>', '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);
}
});
// 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();