diff --git a/README.md b/README.md index faed811..7030ff4 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ bird replies https://x.com/user/status/1234567890123456789 ``` Transport (engine) selection: -- `--engine graphql|sweetistics|auto` (default `auto`). +- `--engine graphql|sweetistics|auto` (default `graphql`). - `sweetistics`: use Sweetistics API key, no browser cookies needed. - `graphql`: use Twitter/X GraphQL with cookies (Chrome/Firefox/env/flags). - `auto`: Sweetistics if an API key is available, otherwise GraphQL. @@ -52,31 +52,28 @@ You can set persistent defaults via config files (JSON5): - Global: `~/.config/bird/config.json5` - Project: `./.birdrc.json5` (overrides global) -Example `~/.config/bird/config.json5`: +Example `~/.config/bird/config.json5` (Firefox + GraphQL defaults): ```json5 { - // Default to Sweetistics unless overridden by --engine or BIRD_ENGINE - engine: "sweetistics", + engine: "graphql", // Prefer Firefox cookies by default firefoxProfile: "default-release", - // Optional: Sweetistics defaults - sweetisticsApiKey: "sweet-...", - sweetisticsBaseUrl: "https://sweetistics.com" + // Optional: Sweetistics defaults if you want fallback/overrides + sweetisticsApiKey: "sweet-..." } ``` Precedence: CLI flags > environment variables > project config > global config. -To default to Firefox and Sweetistics, create `~/.config/bird/config.json5`: +### Credential sources (macOS) -```json5 -{ - engine: "sweetistics", - firefoxProfile: "default-release", - sweetisticsApiKey: "sweet-..." -} -``` +Used only when transport resolves to **graphql**: +- **Firefox (default)**: `~/Library/Application Support/Firefox/Profiles//cookies.sqlite`. `--firefox-profile ` (defaults to `default-release` if present). +- **Chrome**: `~/Library/Application Support/Google/Chrome//Cookies` (WAL/SHM copied too). `--chrome-profile `. +- **Env/flags** always override browser cookies. + +Transport is chosen first; then, if transport is GraphQL, the cookie source is resolved with the same precedence. ### Post a tweet diff --git a/src/index.ts b/src/index.ts index 2864e48..7ba5f9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -98,7 +98,11 @@ program .option('--auth-token ', 'Twitter auth_token cookie') .option('--ct0 ', 'Twitter ct0 cookie') .option('--chrome-profile ', 'Chrome profile name for cookie extraction', config.chromeProfile) - .option('--firefox-profile ', 'Firefox profile name for cookie extraction', config.firefoxProfile) + .option( + '--firefox-profile ', + 'Firefox profile name for cookie extraction', + config.firefoxProfile || 'default-release', + ) .option('--sweetistics-api-key ', 'Sweetistics API key (or set SWEETISTICS_API_KEY)') .option( '--sweetistics-base-url ', @@ -108,7 +112,7 @@ program .option( '--engine ', 'Engine: graphql | sweetistics | auto', - process.env.BIRD_ENGINE || config.engine || 'auto', + process.env.BIRD_ENGINE || config.engine || 'graphql', ); type EngineMode = 'graphql' | 'sweetistics' | 'auto'; @@ -227,6 +231,21 @@ program if (result.success) { console.log('āœ… Tweet posted successfully!'); console.log(`šŸ”— https://x.com/i/status/${result.tweetId}`); + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL tweet failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ + baseUrl: sweetistics.baseUrl, + apiKey: sweetistics.apiKey, + }).tweet(text); + if (fallback.success) { + console.log('āœ… Tweet posted via Sweetistics (fallback)!'); + if (fallback.tweetId) { + console.log(`šŸ”— https://x.com/i/status/${fallback.tweetId}`); + } + } else { + console.error(`āŒ Failed to post tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Failed to post tweet: ${result.error}`); process.exit(1); @@ -303,6 +322,21 @@ program if (result.success) { console.log('āœ… Reply posted successfully!'); console.log(`šŸ”— https://x.com/i/status/${result.tweetId}`); + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL reply failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ + baseUrl: sweetistics.baseUrl, + apiKey: sweetistics.apiKey, + }).tweet(text, tweetId); + if (fallback.success) { + console.log('āœ… Reply posted via Sweetistics (fallback)!'); + if (fallback.tweetId) { + console.log(`šŸ”— https://x.com/i/status/${fallback.tweetId}`); + } + } else { + console.error(`āŒ Failed to post reply: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Failed to post reply: ${result.error}`); process.exit(1); @@ -383,6 +417,28 @@ program `ā¤ļø ${result.tweet.likeCount ?? 0} šŸ” ${result.tweet.retweetCount ?? 0} šŸ’¬ ${result.tweet.replyCount ?? 0}`, ); } + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL read failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).read( + tweetId, + ); + if (fallback.success && fallback.tweet) { + if (cmdOpts.json) { + console.log(JSON.stringify(fallback.tweet, null, 2)); + } else { + console.log(`@${fallback.tweet.author.username} (${fallback.tweet.author.name}):`); + console.log(fallback.tweet.text); + if (fallback.tweet.createdAt) { + console.log(`\nšŸ“… ${fallback.tweet.createdAt}`); + } + console.log( + `ā¤ļø ${fallback.tweet.likeCount ?? 0} šŸ” ${fallback.tweet.retweetCount ?? 0} šŸ’¬ ${fallback.tweet.replyCount ?? 0}`, + ); + } + } else { + console.error(`āŒ Failed to read tweet: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Failed to read tweet: ${result.error}`); process.exit(1); @@ -440,6 +496,18 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' }); + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL replies failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ + baseUrl: sweetistics.baseUrl, + apiKey: sweetistics.apiKey, + }).replies(tweetId); + if (fallback.success && fallback.tweets) { + printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' }); + } else { + console.error(`āŒ Failed to fetch replies: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Failed to fetch replies: ${result.error}`); process.exit(1); @@ -497,6 +565,17 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' }); + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL thread failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).thread( + tweetId, + ); + if (fallback.success && fallback.tweets) { + printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' }); + } else { + console.error(`āŒ Failed to fetch thread: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Failed to fetch thread: ${result.error}`); process.exit(1); @@ -556,6 +635,18 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' }); + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL search failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).search( + query, + count, + ); + if (fallback.success && fallback.tweets) { + printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' }); + } else { + console.error(`āŒ Search failed: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Search failed: ${result.error}`); process.exit(1); @@ -610,6 +701,18 @@ program if (result.success && result.tweets) { printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); + } else if (sweetistics.apiKey) { + console.error(`āš ļø GraphQL mentions failed (${result.error}); trying Sweetistics fallback...`); + const fallback = await new SweetisticsClient({ baseUrl: sweetistics.baseUrl, apiKey: sweetistics.apiKey }).search( + '@clawdbot', + count, + ); + if (fallback.success && fallback.tweets) { + printTweets(fallback.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' }); + } else { + console.error(`āŒ Failed to fetch mentions: ${result.error} | Sweetistics fallback: ${fallback.error}`); + process.exit(1); + } } else { console.error(`āŒ Failed to fetch mentions: ${result.error}`); process.exit(1); @@ -635,14 +738,14 @@ program ? 'Sweetistics API key' : 'none' : opts.chromeProfile - ? `Chrome profile "${opts.chromeProfile}"` - : opts.firefoxProfile - ? `Firefox profile "${opts.firefoxProfile}"` - : config.chromeProfile - ? `Chrome profile "${config.chromeProfile}"` - : config.firefoxProfile - ? `Firefox profile "${config.firefoxProfile}"` - : 'env/auto-detected cookies'; + ? `Chrome profile "${opts.chromeProfile}"` + : opts.firefoxProfile + ? `Firefox profile "${opts.firefoxProfile}"` + : config.chromeProfile + ? `Chrome profile "${config.chromeProfile}"` + : config.firefoxProfile + ? `Firefox profile "${config.firefoxProfile}"` + : 'env/auto-detected cookies'; if (useSweetistics) { if (!sweetistics.apiKey) { @@ -699,6 +802,25 @@ program console.log(`āš™ļø Engine: ${resolvedEngine}`); console.log(`šŸ”‘ Credentials: ${credentialSource}`); } else { + // Fallback: try Sweetistics if available + if (sweetistics.apiKey) { + const fallback = await new SweetisticsClient({ + baseUrl: sweetistics.baseUrl, + apiKey: sweetistics.apiKey, + }).getCurrentUser(); + if (fallback.success && fallback.user) { + const handle = fallback.user.username ? `@${fallback.user.username}` : '(no handle)'; + const name = fallback.user.name || handle; + console.log(`šŸ™‹ Logged in via Sweetistics as ${handle} (${name})`); + console.log(`🪪 User ID: ${fallback.user.id}`); + if (fallback.user.email) { + console.log(`šŸ“§ ${fallback.user.email}`); + } + console.log('āš™ļø Engine: sweetistics (fallback)'); + console.log('šŸ”‘ Credentials: Sweetistics API key'); + return; + } + } console.error(`āŒ Failed to determine current user: ${result.error ?? 'Unknown error'}`); process.exit(1); }