feat: add pagination support to list-timeline command

Adds --all, --max-pages, and --cursor options to list-timeline command, enabling users to fetch all tweets from a list through automatic pagination. Follows the same pattern as bookmarks pagination for consistency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
Zhe Li
2026-01-11 11:17:27 +00:00
committed by Peter Steinberger
co-authored by Claude Sonnet 4.5
parent bda980fa4d
commit 803bd25b0c
3 changed files with 686 additions and 34 deletions
+32 -6
View File
@@ -78,13 +78,17 @@ export function registerListsCommand(program: Command, ctx: CliContext): void {
.command('list-timeline <list-id-or-url>')
.description('Get tweets from a list timeline')
.option('-n, --count <number>', 'Number of tweets to fetch', '20')
.option('--all', 'Fetch all tweets from list (paged)')
.option('--max-pages <number>', 'Stop after N pages when using --all')
.option('--cursor <string>', 'Resume pagination from a cursor')
.option('--json', 'Output as JSON')
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
.action(async (listIdOrUrl: string, cmdOpts: { count?: string; json?: boolean; jsonFull?: boolean }) => {
.action(async (listIdOrUrl: string, cmdOpts: { count?: string; json?: boolean; jsonFull?: boolean; all?: boolean; maxPages?: string; cursor?: string }) => {
const opts = program.opts();
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
const quoteDepth = ctx.resolveQuoteDepthFromOptions(opts);
const count = Number.parseInt(cmdOpts.count || '20', 10);
const maxPages = cmdOpts.maxPages ? Number.parseInt(cmdOpts.maxPages, 10) : undefined;
const listId = extractListId(listIdOrUrl);
if (!listId) {
@@ -92,6 +96,20 @@ export function registerListsCommand(program: Command, ctx: CliContext): void {
process.exit(2);
}
const usePagination = cmdOpts.all || cmdOpts.cursor;
if (maxPages !== undefined && !usePagination) {
console.error(`${ctx.p('err')}--max-pages requires --all or --cursor.`);
process.exit(1);
}
if (!usePagination && (!Number.isFinite(count) || count <= 0)) {
console.error(`${ctx.p('err')}Invalid --count. Expected a positive integer.`);
process.exit(1);
}
if (maxPages !== undefined && (!Number.isFinite(maxPages) || maxPages <= 0)) {
console.error(`${ctx.p('err')}Invalid --max-pages. Expected a positive integer.`);
process.exit(1);
}
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
for (const warning of warnings) {
@@ -104,13 +122,21 @@ export function registerListsCommand(program: Command, ctx: CliContext): void {
}
const client = new TwitterClient({ cookies, timeoutMs, quoteDepth });
const result = await client.getListTimeline(listId, count, { includeRaw: cmdOpts.jsonFull });
const includeRaw = cmdOpts.jsonFull ?? false;
const timelineOptions = { includeRaw };
const paginationOptions = { includeRaw, maxPages, cursor: cmdOpts.cursor };
const result = usePagination
? await client.getAllListTimeline(listId, paginationOptions)
: await client.getListTimeline(listId, count, timelineOptions);
if (result.success && result.tweets) {
ctx.printTweets(result.tweets, {
json: cmdOpts.json || cmdOpts.jsonFull,
emptyMessage: 'No tweets found in this list.',
});
const isJson = cmdOpts.json || cmdOpts.jsonFull;
if (isJson && usePagination) {
console.log(JSON.stringify({ tweets: result.tweets, nextCursor: result.nextCursor ?? null }, null, 2));
} else {
ctx.printTweets(result.tweets, { json: isJson, emptyMessage: 'No tweets found in this list.' });
}
} else {
console.error(`${ctx.p('err')}Failed to fetch list timeline: ${result.error}`);
process.exit(1);