From 3c10a90d091f779a7651e74a0e550125baf0f168 Mon Sep 17 00:00:00 2001 From: entropyy0 Date: Fri, 30 Jan 2026 20:41:29 +1100 Subject: [PATCH] feat: add --quote flag for creating quote tweets Add --quote option to the tweet command for creating proper quote tweets with embedded original tweets. Usage: bird tweet 'My commentary' --quote https://x.com/user/status/123 bird tweet 'My commentary' --quote 123456789 Implementation: - Add quoteTweetId parameter to tweet() in TwitterClient - Pass attachment_url in CreateTweet variables (Twitter's quote API) - Add --quote option to tweet command with extractTweetId support - Accept both tweet URLs and raw IDs Closes #56 --- src/commands/post.ts | 11 ++++++++--- src/lib/twitter-client-posting.ts | 10 +++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/commands/post.ts b/src/commands/post.ts index e39401b..412538d 100644 --- a/src/commands/post.ts +++ b/src/commands/post.ts @@ -29,7 +29,8 @@ export function registerPostCommands(program: Command, ctx: CliContext): void { .command('tweet') .description('Post a new tweet') .argument('', 'Tweet text') - .action(async (text: string) => { + .option('--quote ', 'Quote tweet: embed the given tweet') + .action(async (text: string, cmdOpts: { quote?: string }) => { const opts = program.opts(); const timeoutMs = ctx.resolveTimeoutFromOptions(opts); const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts); @@ -58,10 +59,14 @@ export function registerPostCommands(program: Command, ctx: CliContext): void { const client = new TwitterClient({ cookies, timeoutMs, quoteDepth }); const mediaIds = await uploadMediaOrExit(client, media, ctx); - const result = await client.tweet(text, mediaIds); + const quoteTweetId = cmdOpts.quote ? ctx.extractTweetId(cmdOpts.quote) : undefined; + if (quoteTweetId) { + console.error(`${ctx.p('info')}Quoting tweet: ${quoteTweetId}`); + } + const result = await client.tweet(text, mediaIds, quoteTweetId); if (result.success) { - console.log(`${ctx.p('ok')}Tweet posted successfully!`); + console.log(`${ctx.p('ok')}${quoteTweetId ? 'Quote tweet' : 'Tweet'} posted successfully!`); console.log(formatTweetUrlLine(result.tweetId, ctx.getOutput())); } else { console.error(`${ctx.p('err')}Failed to post tweet: ${result.error}`); diff --git a/src/lib/twitter-client-posting.ts b/src/lib/twitter-client-posting.ts index 693ffe6..33642eb 100644 --- a/src/lib/twitter-client-posting.ts +++ b/src/lib/twitter-client-posting.ts @@ -4,7 +4,7 @@ import { buildTweetCreateFeatures } from './twitter-client-features.js'; import type { CreateTweetResponse, TweetResult } from './twitter-client-types.js'; export interface TwitterClientPostingMethods { - tweet(text: string, mediaIds?: string[]): Promise; + tweet(text: string, mediaIds?: string[], quoteTweetId?: string): Promise; reply(text: string, replyToTweetId: string, mediaIds?: string[]): Promise; } @@ -20,8 +20,8 @@ export function withPosting /** * Post a new tweet */ - async tweet(text: string, mediaIds?: string[]): Promise { - const variables = { + async tweet(text: string, mediaIds?: string[], quoteTweetId?: string): Promise { + const variables: Record = { tweet_text: text, dark_request: false, media: { @@ -31,6 +31,10 @@ export function withPosting semantic_annotation_ids: [], }; + if (quoteTweetId) { + variables.attachment_url = `https://x.com/i/web/status/${quoteTweetId}`; + } + const features = buildTweetCreateFeatures(); return this.createTweet(variables, features);