feat: add --json-full flag to include raw API response in _raw field

Add a new --json-full flag to tweet-fetching commands that outputs JSON
with an additional _raw field containing the full GraphQL response.

This provides power users access to additional data not exposed in the
curated TweetData interface (media, entities, hashtags, cards, etc.)
while maintaining backward compatibility with --json.

Supported commands:
- read, replies, thread (TweetDetail API)
- search, mentions (SearchTimeline API)
- bookmarks (Bookmarks API)
- likes (Likes API)

Not supported: following, followers (uses mixed GraphQL/REST fallback
with different response structures)

Implementation:
- Add _raw?: GraphqlTweetResult optional field to TweetData type
- Add includeRaw option to mapTweetResult() and parseTweetsFromInstructions()
- Add TweetFetchOptions, SearchFetchOptions, TimelineFetchOptions interfaces
- Update client methods to accept options parameter
- Add --json-full flag to CLI commands
- Update help text to document the new flag
- Add comprehensive test coverage (16 tests)

Co-authored-by: Christian Catalan <[email protected]>
This commit is contained in:
cc-vps
2026-01-03 06:54:36 +01:00
committed by Peter Steinberger
co-authored by Christian Catalan
parent 42abae9c15
commit 3815ebfa03
13 changed files with 658 additions and 48 deletions
+2
View File
@@ -82,6 +82,8 @@ export function createProgram(ctx: CliContext): Command {
formatExample('bird <tweet-id-or-url> [--json]', 'Shorthand for `bird read <tweet-id-or-url>`'),
].join('\n\n')}\n\n${ctx.colors.section('JSON Output')}\n${ctx.colors.muted(
` Add ${ctx.colors.option('--json')} to: read, replies, thread, search, mentions, bookmarks, likes, following, followers, query-ids`,
)}\n${ctx.colors.muted(
` Add ${ctx.colors.option('--json-full')} to include raw API response in ${ctx.colors.argument('_raw')} field (tweet commands only)`,
)}\n${ctx.colors.muted(` (Run ${ctx.colors.command('bird <command> --help')} to see per-command flags.)`)}`,
);
+6 -4
View File
@@ -10,7 +10,8 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
.option('-n, --count <number>', 'Number of bookmarks to fetch', '20')
.option('--folder-id <id>', 'Bookmark folder (collection) id')
.option('--json', 'Output as JSON')
.action(async (cmdOpts: { count?: string; json?: boolean; folderId?: string }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (cmdOpts: { count?: string; json?: boolean; jsonFull?: boolean; folderId?: string }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const count = Number.parseInt(cmdOpts.count || '20', 10);
@@ -32,13 +33,14 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
console.error(`${ctx.p('err')}Invalid --folder-id. Expected numeric ID or https://x.com/i/bookmarks/<id>.`);
process.exit(1);
}
const includeRaw = cmdOpts.jsonFull ?? false;
const result = folderId
? await client.getBookmarkFolderTimeline(folderId, count)
: await client.getBookmarks(count);
? await client.getBookmarkFolderTimeline(folderId, count, { includeRaw })
: await client.getBookmarks(count, { includeRaw });
if (result.success && result.tweets) {
const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.';
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage });
ctx.printTweets(result.tweets, { json: cmdOpts.json || cmdOpts.jsonFull, emptyMessage });
} else {
console.error(`${ctx.p('err')}Failed to fetch bookmarks: ${result.error}`);
process.exit(1);
+18 -9
View File
@@ -9,7 +9,8 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
.description('Read/fetch a tweet by ID or URL')
.argument('<tweet-id-or-url>', 'Tweet ID or URL to read')
.option('--json', 'Output as JSON')
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean; jsonFull?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
@@ -28,10 +29,11 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const result = await client.getTweet(tweetId);
const includeRaw = cmdOpts.jsonFull ?? false;
const result = await client.getTweet(tweetId, { includeRaw });
if (result.success && result.tweet) {
if (cmdOpts.json) {
if (cmdOpts.json || cmdOpts.jsonFull) {
console.log(JSON.stringify(result.tweet, null, 2));
} else {
console.log(`@${result.tweet.author.username} (${result.tweet.author.name}):`);
@@ -52,7 +54,8 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
.description('List replies to a tweet (by ID or URL)')
.argument('<tweet-id-or-url>', 'Tweet ID or URL')
.option('--json', 'Output as JSON')
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean; jsonFull?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
@@ -70,10 +73,11 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const result = await client.getReplies(tweetId);
const includeRaw = cmdOpts.jsonFull ?? false;
const result = await client.getReplies(tweetId, { includeRaw });
if (result.success && result.tweets) {
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No replies found.' });
ctx.printTweets(result.tweets, { json: cmdOpts.json || cmdOpts.jsonFull, emptyMessage: 'No replies found.' });
} else {
console.error(`${ctx.p('err')}Failed to fetch replies: ${result.error}`);
process.exit(1);
@@ -85,7 +89,8 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
.description('Show the full conversation thread containing the tweet')
.argument('<tweet-id-or-url>', 'Tweet ID or URL')
.option('--json', 'Output as JSON')
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (tweetIdOrUrl: string, cmdOpts: { json?: boolean; jsonFull?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
@@ -103,10 +108,14 @@ export function registerReadCommands(program: Command, ctx: CliContext): void {
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const result = await client.getThread(tweetId);
const includeRaw = cmdOpts.jsonFull ?? false;
const result = await client.getThread(tweetId, { includeRaw });
if (result.success && result.tweets) {
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No thread tweets found.' });
ctx.printTweets(result.tweets, {
json: cmdOpts.json || cmdOpts.jsonFull,
emptyMessage: 'No thread tweets found.',
});
} else {
console.error(`${ctx.p('err')}Failed to fetch thread: ${result.error}`);
process.exit(1);
+10 -6
View File
@@ -10,7 +10,8 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
.argument('<query>', 'Search query (e.g., "@clawdbot" or "from:clawdbot")')
.option('-n, --count <number>', 'Number of tweets to fetch', '10')
.option('--json', 'Output as JSON')
.action(async (query: string, cmdOpts: { count?: string; json?: boolean }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (query: string, cmdOpts: { count?: string; json?: boolean; jsonFull?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
@@ -28,10 +29,11 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const result = await client.search(query, count);
const includeRaw = cmdOpts.jsonFull ?? false;
const result = await client.search(query, count, { includeRaw });
if (result.success && result.tweets) {
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No tweets found.' });
ctx.printTweets(result.tweets, { json: cmdOpts.json || cmdOpts.jsonFull, emptyMessage: 'No tweets found.' });
} else {
console.error(`${ctx.p('err')}Search failed: ${result.error}`);
process.exit(1);
@@ -44,7 +46,8 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
.option('-u, --user <handle>', 'User handle (e.g. @steipete)')
.option('-n, --count <number>', 'Number of tweets to fetch', '10')
.option('--json', 'Output as JSON')
.action(async (cmdOpts: { user?: string; count?: string; json?: boolean }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (cmdOpts: { user?: string; count?: string; json?: boolean; jsonFull?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
@@ -84,10 +87,11 @@ export function registerSearchCommands(program: Command, ctx: CliContext): void
}
}
const result = await client.search(query, count);
const includeRaw = cmdOpts.jsonFull ?? false;
const result = await client.search(query, count, { includeRaw });
if (result.success && result.tweets) {
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No mentions found.' });
ctx.printTweets(result.tweets, { json: cmdOpts.json || cmdOpts.jsonFull, emptyMessage: 'No mentions found.' });
} else {
console.error(`${ctx.p('err')}Failed to fetch mentions: ${result.error}`);
process.exit(1);
+8 -3
View File
@@ -130,7 +130,8 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
.description('Get your liked tweets')
.option('-n, --count <number>', 'Number of likes to fetch', '20')
.option('--json', 'Output as JSON')
.action(async (cmdOpts: { count?: string; json?: boolean }) => {
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (cmdOpts: { count?: string; json?: boolean; jsonFull?: boolean }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
@@ -148,10 +149,14 @@ export function registerUserCommands(program: Command, ctx: CliContext): void {
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const result = await client.getLikes(count);
const includeRaw = cmdOpts.jsonFull ?? false;
const result = await client.getLikes(count, { includeRaw });
if (result.success && result.tweets) {
ctx.printTweets(result.tweets, { json: cmdOpts.json, emptyMessage: 'No liked tweets found.' });
ctx.printTweets(result.tweets, {
json: cmdOpts.json || cmdOpts.jsonFull,
emptyMessage: 'No liked tweets found.',
});
} else {
console.error(`${ctx.p('err')}Failed to fetch likes: ${result.error}`);
process.exit(1);
+3
View File
@@ -18,4 +18,7 @@ export {
type TwitterClientOptions,
type TwitterUser,
} from './twitter-client.js';
export type { SearchFetchOptions } from './twitter-client-search.js';
export type { TimelineFetchOptions } from './twitter-client-timelines.js';
export type { TweetFetchOptions } from './twitter-client-tweet-detail.js';
export type { TweetResult, UploadMediaResult } from './twitter-client-types.js';
+10 -3
View File
@@ -4,8 +4,14 @@ import { buildSearchFeatures } from './twitter-client-features.js';
import type { SearchResult, TweetData } from './twitter-client-types.js';
import { extractCursorFromInstructions, parseTweetsFromInstructions } from './twitter-client-utils.js';
/** Options for search methods */
export interface SearchFetchOptions {
/** Include raw GraphQL response in `_raw` field */
includeRaw?: boolean;
}
export interface TwitterClientSearchMethods {
search(query: string, count?: number): Promise<SearchResult>;
search(query: string, count?: number, options?: SearchFetchOptions): Promise<SearchResult>;
}
export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>(
@@ -20,7 +26,8 @@ export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>
/**
* Search for tweets matching a query
*/
async search(query: string, count = 20): Promise<SearchResult> {
async search(query: string, count = 20, options: SearchFetchOptions = {}): Promise<SearchResult> {
const { includeRaw = false } = options;
const features = buildSearchFeatures();
const pageSize = 20;
const seen = new Set<string>();
@@ -113,7 +120,7 @@ export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>
}
const instructions = data.data?.search_by_raw_query?.search_timeline?.timeline?.instructions;
const pageTweets = parseTweetsFromInstructions(instructions, this.quoteDepth);
const pageTweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
const nextCursor = extractCursorFromInstructions(instructions);
return { success: true as const, tweets: pageTweets, cursor: nextCursor, had404 };
+22 -9
View File
@@ -4,10 +4,16 @@ import { buildBookmarksFeatures, buildLikesFeatures } from './twitter-client-fea
import type { GraphqlTweetResult, SearchResult } from './twitter-client-types.js';
import { parseTweetsFromInstructions } from './twitter-client-utils.js';
/** Options for timeline fetch methods */
export interface TimelineFetchOptions {
/** Include raw GraphQL response in `_raw` field */
includeRaw?: boolean;
}
export interface TwitterClientTimelineMethods {
getBookmarks(count?: number): Promise<SearchResult>;
getLikes(count?: number): Promise<SearchResult>;
getBookmarkFolderTimeline(folderId: string, count?: number): Promise<SearchResult>;
getBookmarks(count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
getLikes(count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
getBookmarkFolderTimeline(folderId: string, count?: number, options?: TimelineFetchOptions): Promise<SearchResult>;
}
export function withTimelines<TBase extends AbstractConstructor<TwitterClientBase>>(
@@ -37,7 +43,8 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
/**
* Get the authenticated user's bookmarks
*/
async getBookmarks(count = 20): Promise<SearchResult> {
async getBookmarks(count = 20, options: TimelineFetchOptions = {}): Promise<SearchResult> {
const { includeRaw = false } = options;
const variables = {
count,
includePromotedContent: false,
@@ -104,7 +111,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
}
const instructions = data.data?.bookmark_timeline_v2?.timeline?.instructions;
const tweets = parseTweetsFromInstructions(instructions, this.quoteDepth);
const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
return { success: true as const, tweets, had404 };
} catch (error) {
@@ -135,7 +142,8 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
/**
* Get the authenticated user's liked tweets
*/
async getLikes(count = 20): Promise<SearchResult> {
async getLikes(count = 20, options: TimelineFetchOptions = {}): Promise<SearchResult> {
const { includeRaw = false } = options;
const userResult = await this.getCurrentUser();
if (!userResult.success || !userResult.user) {
return { success: false, error: userResult.error ?? 'Could not determine current user' };
@@ -212,7 +220,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
}
const instructions = data.data?.user?.result?.timeline?.timeline?.instructions;
const tweets = parseTweetsFromInstructions(instructions, this.quoteDepth);
const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
return { success: true as const, tweets, had404 };
} catch (error) {
@@ -243,7 +251,12 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
/**
* Get the authenticated user's bookmark folder timeline
*/
async getBookmarkFolderTimeline(folderId: string, count = 20): Promise<SearchResult> {
async getBookmarkFolderTimeline(
folderId: string,
count = 20,
options: TimelineFetchOptions = {},
): Promise<SearchResult> {
const { includeRaw = false } = options;
const variablesWithCount = {
bookmark_collection_id: folderId,
includePromotedContent: true,
@@ -313,7 +326,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
}
const instructions = data.data?.bookmark_collection_timeline?.timeline?.instructions;
const tweets = parseTweetsFromInstructions(instructions, this.quoteDepth);
const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
return { success: true as const, tweets, had404 };
} catch (error) {
+18 -9
View File
@@ -10,10 +10,16 @@ import {
parseTweetsFromInstructions,
} from './twitter-client-utils.js';
/** Options for tweet fetching methods */
export interface TweetFetchOptions {
/** Include raw GraphQL response in `_raw` field */
includeRaw?: boolean;
}
export interface TwitterClientTweetDetailMethods {
getTweet(tweetId: string): Promise<GetTweetResult>;
getReplies(tweetId: string): Promise<SearchResult>;
getThread(tweetId: string): Promise<SearchResult>;
getTweet(tweetId: string, options?: TweetFetchOptions): Promise<GetTweetResult>;
getReplies(tweetId: string, options?: TweetFetchOptions): Promise<SearchResult>;
getThread(tweetId: string, options?: TweetFetchOptions): Promise<SearchResult>;
}
export function withTweetDetails<TBase extends AbstractConstructor<TwitterClientBase>>(
@@ -246,7 +252,8 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
/**
* Get tweet details by ID
*/
async getTweet(tweetId: string): Promise<GetTweetResult> {
async getTweet(tweetId: string, options: TweetFetchOptions = {}): Promise<GetTweetResult> {
const { includeRaw = false } = options;
const response = await this.fetchTweetDetail(tweetId);
if (!response.success) {
return response;
@@ -271,7 +278,7 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
tweetId,
);
const mapped = mapTweetResult(tweetResult, this.quoteDepth);
const mapped = mapTweetResult(tweetResult, { quoteDepth: this.quoteDepth, includeRaw });
if (mapped) {
if (tweetResult?.article) {
const title = firstText(tweetResult.article.article_results?.result?.title, tweetResult.article.title);
@@ -294,14 +301,15 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
/**
* Get replies to a tweet by ID
*/
async getReplies(tweetId: string): Promise<SearchResult> {
async getReplies(tweetId: string, options: TweetFetchOptions = {}): Promise<SearchResult> {
const { includeRaw = false } = options;
const response = await this.fetchTweetDetail(tweetId);
if (!response.success) {
return response;
}
const instructions = response.data.threaded_conversation_with_injections_v2?.instructions;
const tweets = parseTweetsFromInstructions(instructions, this.quoteDepth);
const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
const replies = tweets.filter((tweet) => tweet.inReplyToStatusId === tweetId);
return { success: true, tweets: replies };
@@ -310,14 +318,15 @@ export function withTweetDetails<TBase extends AbstractConstructor<TwitterClient
/**
* Get full conversation thread for a tweet ID
*/
async getThread(tweetId: string): Promise<SearchResult> {
async getThread(tweetId: string, options: TweetFetchOptions = {}): Promise<SearchResult> {
const { includeRaw = false } = options;
const response = await this.fetchTweetDetail(tweetId);
if (!response.success) {
return response;
}
const instructions = response.data.threaded_conversation_with_injections_v2?.instructions;
const tweets = parseTweetsFromInstructions(instructions, this.quoteDepth);
const tweets = parseTweetsFromInstructions(instructions, { quoteDepth: this.quoteDepth, includeRaw });
const target = tweets.find((t) => t.id === tweetId);
const rootId = target?.conversationId || tweetId;
+3
View File
@@ -184,6 +184,9 @@ export interface TweetData {
inReplyToStatusId?: string;
// Optional quoted tweet; depth controlled by quoteDepth (default: 1).
quotedTweet?: TweetData;
// Raw GraphQL response (included when --json-full is used).
// Structure may change as Twitter's API evolves.
_raw?: GraphqlTweetResult;
}
export interface GetTweetResult {
+32 -5
View File
@@ -163,7 +163,19 @@ export function unwrapTweetResult(result: GraphqlTweetResult | undefined): Graph
return result;
}
export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDepth: number): TweetData | undefined {
export interface MapTweetResultOptions {
quoteDepth: number;
includeRaw?: boolean;
}
export function mapTweetResult(
result: GraphqlTweetResult | undefined,
quoteDepthOrOptions: number | MapTweetResultOptions,
): TweetData | undefined {
const options: MapTweetResultOptions =
typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions;
const { quoteDepth, includeRaw = false } = options;
const userResult = result?.core?.user_results?.result;
const userLegacy = userResult?.legacy;
const userCore = userResult?.core;
@@ -183,11 +195,11 @@ export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDept
if (quoteDepth > 0) {
const quotedResult = unwrapTweetResult(result.quoted_status_result?.result);
if (quotedResult) {
quotedTweet = mapTweetResult(quotedResult, quoteDepth - 1);
quotedTweet = mapTweetResult(quotedResult, { quoteDepth: quoteDepth - 1, includeRaw });
}
}
return {
const tweetData: TweetData = {
id: result.rest_id,
text,
createdAt: result.legacy?.created_at,
@@ -203,6 +215,12 @@ export function mapTweetResult(result: GraphqlTweetResult | undefined, quoteDept
authorId: userId,
quotedTweet,
};
if (includeRaw) {
tweetData._raw = result;
}
return tweetData;
}
export function findTweetInInstructions(
@@ -294,6 +312,11 @@ export function collectTweetResultsFromEntry(entry: {
return results;
}
export interface ParseTweetsOptions {
quoteDepth: number;
includeRaw?: boolean;
}
export function parseTweetsFromInstructions(
instructions:
| Array<{
@@ -336,8 +359,12 @@ export function parseTweetsFromInstructions(
}>;
}>
| undefined,
quoteDepth: number,
quoteDepthOrOptions: number | ParseTweetsOptions,
): TweetData[] {
const options: ParseTweetsOptions =
typeof quoteDepthOrOptions === 'number' ? { quoteDepth: quoteDepthOrOptions } : quoteDepthOrOptions;
const { quoteDepth, includeRaw = false } = options;
const tweets: TweetData[] = [];
const seen = new Set<string>();
@@ -345,7 +372,7 @@ export function parseTweetsFromInstructions(
for (const entry of instruction.entries ?? []) {
const results = collectTweetResultsFromEntry(entry);
for (const result of results) {
const mapped = mapTweetResult(result, quoteDepth);
const mapped = mapTweetResult(result, { quoteDepth, includeRaw });
if (!mapped || seen.has(mapped.id)) {
continue;
}