fix: tighten news output and parsing (#39) (thanks @aavetis)

This commit is contained in:
Peter Steinberger
2026-01-12 05:37:44 +00:00
parent fb092f5cec
commit c96ce5238f
5 changed files with 146 additions and 25 deletions
+20 -8
View File
@@ -16,7 +16,7 @@ function formatPostCount(count: number): string {
function printNewsItems(
items: NewsItem[],
ctx: CliContext,
opts: { json?: boolean; emptyMessage?: string } = {},
opts: { json?: boolean; emptyMessage?: string; tweetLimit?: number } = {},
): void {
if (opts.json) {
console.log(JSON.stringify(items, null, 2));
@@ -54,7 +54,8 @@ function printNewsItems(
// Print related tweets if available
if (item.tweets && item.tweets.length > 0) {
console.log(` ${ctx.colors.section('Related tweets:')}`);
for (const tweet of item.tweets.slice(0, 3)) {
const tweetLimit = opts.tweetLimit ?? item.tweets.length;
for (const tweet of item.tweets.slice(0, tweetLimit)) {
console.log(
` @${tweet.author.username}: ${tweet.text.slice(0, 100)}${tweet.text.length > 100 ? '...' : ''}`,
);
@@ -69,7 +70,7 @@ export function registerNewsCommand(program: Command, ctx: CliContext): void {
program
.command('news')
.alias('trending')
.description("Fetch AI-curated news and trending topics from Explore tabs")
.description('Fetch AI-curated news and trending topics from Explore tabs')
.option('-n, --count <number>', 'Number of items to fetch', '10')
.option('--ai-only', 'Show only AI-curated news items')
.option('--with-tweets', 'Also fetch related tweets for each news item')
@@ -124,11 +125,21 @@ export function registerNewsCommand(program: Command, ctx: CliContext): void {
// Determine which tabs to fetch from
const tabs: ExploreTab[] = [];
if (cmdOpts.forYou) tabs.push('forYou');
if (cmdOpts.newsOnly) tabs.push('news');
if (cmdOpts.sports) tabs.push('sports');
if (cmdOpts.entertainment) tabs.push('entertainment');
if (cmdOpts.trendingOnly) tabs.push('trending');
if (cmdOpts.forYou) {
tabs.push('forYou');
}
if (cmdOpts.newsOnly) {
tabs.push('news');
}
if (cmdOpts.sports) {
tabs.push('sports');
}
if (cmdOpts.entertainment) {
tabs.push('entertainment');
}
if (cmdOpts.trendingOnly) {
tabs.push('trending');
}
// If no specific tabs selected, use defaults (all tabs except trending)
const tabsToFetch = tabs.length > 0 ? tabs : undefined;
@@ -150,6 +161,7 @@ export function registerNewsCommand(program: Command, ctx: CliContext): void {
printNewsItems(result.items, ctx, {
json: cmdOpts.json || cmdOpts.jsonFull,
emptyMessage: 'No news items found.',
tweetLimit: withTweets ? tweetsPerItem : undefined,
});
} else {
console.error(`${ctx.p('err')}Failed to fetch news: ${result.error}`);
+9 -13
View File
@@ -96,7 +96,7 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
}
try {
const tabItems = await this.fetchTimelineTab(tab, timelineId, count, aiOnly, includeRaw, debug);
const tabItems = await this.fetchTimelineTab(tab, timelineId, count, aiOnly, includeRaw);
// Deduplicate across tabs
for (const item of tabItems) {
@@ -107,9 +107,7 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
}
if (debug) {
console.error(
`[getNews] Tab ${tab}: found ${tabItems.length} items, total unique: ${allItems.length}`,
);
console.error(`[getNews] Tab ${tab}: found ${tabItems.length} items, total unique: ${allItems.length}`);
}
// Stop early if we have enough
@@ -147,7 +145,6 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
maxCount: number,
aiOnly: boolean,
includeRaw: boolean,
debug: boolean,
): Promise<NewsItem[]> {
const queryId = await this.getQueryId('GenericTimelineById');
const features = buildExploreFeatures();
@@ -220,12 +217,11 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
const instructions = timeline.instructions || [];
for (const instruction of instructions) {
if (instruction.type !== 'TimelineAddEntries') {
const entries = instruction.entries ?? (instruction.entry ? [instruction.entry] : []);
if (!entries || entries.length === 0) {
continue;
}
const entries = instruction.entries || [];
for (const entry of entries) {
if (items.length >= maxCount) {
break;
@@ -285,8 +281,6 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
return items;
}
// biome-ignore lint/suspicious/noExplicitAny: API response structure is complex
private parseNewsItemFromContent(
// biome-ignore lint/suspicious/noExplicitAny: API response structure is complex
itemContent: any,
@@ -302,6 +296,9 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
return null;
}
const trendMetadata = itemContent?.trend_metadata;
const trendUrl = itemContent.trend_url?.url || trendMetadata?.url?.url;
// Detect AI news by characteristics:
// 1. Full sentence headlines (contains spaces and is longer)
// 2. Has social_context with "News" category
@@ -360,7 +357,6 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
}
// Parse trend metadata
const trendMetadata = itemContent?.trend_metadata;
if (trendMetadata?.meta_description) {
const metaDesc = trendMetadata.meta_description;
const postMatch = metaDesc.match(POST_COUNT_MATCH_REGEX);
@@ -385,13 +381,13 @@ export function withNews<TBase extends AbstractConstructor<TwitterClientBase>>(
}
const item: NewsItem = {
id: entryId || `${source}-${headline}`,
id: trendUrl ?? (entryId ? `${entryId}-${headline}` : `${source}-${headline}`),
headline,
category: isAiNews ? `AI · ${category}` : category,
timeAgo,
postCount,
description: itemContent.description,
url: itemContent.trend_url?.url || trendMetadata?.url?.url,
url: trendUrl,
};
if (includeRaw) {