feat(hf): add top command for hf papers (daily, weekly, monthly) (#110)

* feat(hf): add top command for hf papers (daily, weekly, monthly)

* feat(footer): add footerExtra support and derive dates from API response

Add footerExtra callback to CliCommand for custom table footer content.
For weekly/monthly periods, derive date range from API response publishedAt
field with local clock fallback.

* fix: truncate long paper titles

* refactor(hf): remove comments column for consistent output

* feat(hf): add --all flag to return all papers

* feat(hf): add paper id column to output

* fix: restore main.ts as bootstrap, sync footerExtra + CDPBridge + domain pre-nav to cli.ts

- main.ts should remain a lightweight entry point delegating to cli.ts
- Preserve CDPBridge fallback (OPENCLI_CDP_ENDPOINT) — PR had hardcoded BrowserBridge only
- Add domain pre-navigation for cookie/header strategies to cli.ts
- footerExtra feature from PR is properly integrated

---------

Co-authored-by: jackwener <jakevingoo@gmail.com>
This commit is contained in:
K1tyooandjackwener authored and GitHub committed 2026-03-20 14:23:06 +08:00
1 parent 943e286815
commit 024d9908b3
4 files changed
+152 -2

No files matched your search

+7 -2
View File
@@ -1,7 +1,7 @@
import { Command } from 'commander';
import chalk from 'chalk';
import { executeCommand } from './engine.js';
import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
import { render as renderOutput } from './output.js';
import { BrowserBridge, CDPBridge } from './browser/index.js';
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
@@ -162,13 +162,18 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
if (shouldUseBrowserSession(cmd)) {
const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
result = await browserSession(BrowserFactory as any, async (page) => {
// Cookie/header strategies require same-origin context for credentialed fetch.
if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
}
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
}, { workspace: `site:${cmd.site}` });
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
}
renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
renderOutput(result, { fmt: actionOpts.format, columns: resolved.columns, title: `${resolved.site}/${resolved.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(resolved), footerExtra: resolved.footerExtra?.(kwargs) });
} catch (err: any) {
if (err instanceof CliError) {
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
+141
View File
@@ -0,0 +1,141 @@
import { cli, Strategy } from '../../registry.js';
import { CliError } from '../../errors.js';
interface PaperAuthor {
name: string;
}
interface DailyPaper {
paper: {
id: string;
upvotes: number;
authors: PaperAuthor[];
};
title: string;
numComments: number;
}
interface PeriodPaper {
id: string;
title: string;
upvotes: number;
publishedAt: string;
authors: PaperAuthor[];
}
function truncate(str: string, max = 60): string {
return str.length > max ? str.slice(0, max - 3) + '...' : str;
}
function formatAuthors(authors: PaperAuthor[], max = 3): string {
const names = authors.map((a) => a.name);
if (names.length <= max) return names.join(', ');
return names.slice(0, max).join(', ') + ' et al.';
}
const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function getMonthRange(): string {
const now = new Date();
return `${MONTH_ABBR[now.getUTCMonth()]} ${now.getUTCFullYear()}`;
}
function getWeekRange(): string {
const now = new Date();
const day = now.getUTCDay(); // 0=Sun, 6=Sat
const daysToSat = day === 6 ? 0 : 6 - day;
const end = new Date(now);
end.setUTCDate(now.getUTCDate() + daysToSat);
const start = new Date(end);
start.setUTCDate(end.getUTCDate() - 6);
const sm = MONTH_ABBR[start.getUTCMonth()];
const em = MONTH_ABBR[end.getUTCMonth()];
const sd = start.getUTCDate();
const ed = end.getUTCDate();
return sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
}
cli({
site: 'hf',
name: 'top',
description: 'Top upvoted Hugging Face papers',
domain: 'huggingface.co',
strategy: Strategy.PUBLIC,
browser: false,
args: [
{ name: 'limit', type: 'int', default: 20, help: 'Number of papers' },
{ name: 'all', type: 'bool', default: false, help: 'Return all papers (ignore limit)' },
{ name: 'date', type: 'str', required: false, help: 'Date (YYYY-MM-DD), defaults to most recent' },
{ name: 'period', type: 'str', default: 'daily', choices: ['daily', 'weekly', 'monthly'], help: 'Time period: daily, weekly, or monthly' },
],
footerExtra: (kwargs) => {
if (kwargs._footerDate) return kwargs._footerDate;
if (kwargs.period === 'monthly') return getMonthRange();
if (kwargs.period === 'weekly') return getWeekRange();
return kwargs.date ?? new Date().toISOString().slice(0, 10);
},
func: async (_page, kwargs) => {
const period = String(kwargs.period ?? 'daily');
const all = Boolean(kwargs.all);
const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co';
if (period === 'weekly' || period === 'monthly') {
if (kwargs.date) {
throw new CliError('INVALID_ARG', `--date is not supported for ${period} period`, `Omit --date when using --period ${period}`);
}
const url = `${endpoint}/api/papers?period=${period}`;
const res = await fetch(url);
if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
const body = await res.json();
if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check endpoint');
const data: PeriodPaper[] = body;
const dates = data.map((d) => d.publishedAt).filter(Boolean).sort();
if (dates.length > 0) {
if (period === 'monthly') {
const d = new Date(dates[0]);
kwargs._footerDate = `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
} else {
const start = new Date(dates[0]);
const end = new Date(dates[dates.length - 1]);
const sm = MONTH_ABBR[start.getUTCMonth()];
const em = MONTH_ABBR[end.getUTCMonth()];
const sd = start.getUTCDate();
const ed = end.getUTCDate();
kwargs._footerDate = sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`;
}
}
const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0));
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
return items.map((item, i) => ({
rank: i + 1,
id: item.id ?? '',
title: truncate(item.title ?? ''),
upvotes: item.upvotes ?? 0,
authors: formatAuthors(item.authors ?? []),
}));
}
// daily
if (kwargs.date && !/^\d{4}-\d{2}-\d{2}$/.test(String(kwargs.date))) {
throw new CliError('INVALID_ARG', `Invalid date format: ${kwargs.date}`, 'Use YYYY-MM-DD');
}
const url = kwargs.date
? `${endpoint}/api/daily_papers?date=${kwargs.date}`
: `${endpoint}/api/daily_papers`;
const res = await fetch(url);
if (!res.ok) throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later');
const body = await res.json();
if (!Array.isArray(body)) throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check date format or endpoint');
const data: DailyPaper[] = body;
const sorted = [...data].sort((a, b) => (b.paper?.upvotes ?? 0) - (a.paper?.upvotes ?? 0));
const items = all ? sorted : sorted.slice(0, Number(kwargs.limit));
return items.map((item, i) => ({
rank: i + 1,
id: item.paper?.id ?? '',
title: truncate(item.title ?? ''),
upvotes: item.paper?.upvotes ?? 0,
authors: formatAuthors(item.paper?.authors ?? []),
}));
},
});
+2
View File
@@ -12,6 +12,7 @@ export interface RenderOptions {
title?: string;
elapsed?: number;
source?: string;
footerExtra?: string;
}
export function render(data: unknown, opts: RenderOptions = {}): void {
@@ -56,6 +57,7 @@ function renderTable(data: unknown, opts: RenderOptions): void {
footer.push(`${rows.length} items`);
if (opts.elapsed) footer.push(`${opts.elapsed.toFixed(1)}s`);
if (opts.source) footer.push(opts.source);
if (opts.footerExtra) footer.push(opts.footerExtra);
console.log(chalk.dim(footer.join(' · ')));
}
+2
View File
@@ -35,6 +35,7 @@ export interface CliCommand {
pipeline?: Record<string, unknown>[];
timeoutSeconds?: number;
source?: string;
footerExtra?: (kwargs: Record<string, any>) => string | undefined;
}
/** Internal extension for lazy-loaded TS modules (not exposed in public API) */
@@ -63,6 +64,7 @@ export function cli(opts: CliOptions): CliCommand {
func: opts.func,
pipeline: opts.pipeline,
timeoutSeconds: opts.timeoutSeconds,
footerExtra: opts.footerExtra,
};
const key = fullName(cmd);