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:
1 parent
943e286815
commit
024d9908b3
4 files changed
+152
-2
No files matched your search
+7
-2
@@ -1,7 +1,7 @@
|
|||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { executeCommand } from './engine.js';
|
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 { render as renderOutput } from './output.js';
|
||||||
import { BrowserBridge, CDPBridge } from './browser/index.js';
|
import { BrowserBridge, CDPBridge } from './browser/index.js';
|
||||||
import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.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)) {
|
if (shouldUseBrowserSession(cmd)) {
|
||||||
const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
|
const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
|
||||||
result = await browserSession(BrowserFactory as any, async (page) => {
|
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) });
|
return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
|
||||||
}, { workspace: `site:${cmd.site}` });
|
}, { workspace: `site:${cmd.site}` });
|
||||||
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
|
} else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
|
||||||
if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
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.`));
|
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) {
|
} catch (err: any) {
|
||||||
if (err instanceof CliError) {
|
if (err instanceof CliError) {
|
||||||
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|
console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
|
||||||
|
|||||||
@@ -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 ?? []),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -12,6 +12,7 @@ export interface RenderOptions {
|
|||||||
title?: string;
|
title?: string;
|
||||||
elapsed?: number;
|
elapsed?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
footerExtra?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render(data: unknown, opts: RenderOptions = {}): void {
|
export function render(data: unknown, opts: RenderOptions = {}): void {
|
||||||
@@ -56,6 +57,7 @@ function renderTable(data: unknown, opts: RenderOptions): void {
|
|||||||
footer.push(`${rows.length} items`);
|
footer.push(`${rows.length} items`);
|
||||||
if (opts.elapsed) footer.push(`${opts.elapsed.toFixed(1)}s`);
|
if (opts.elapsed) footer.push(`${opts.elapsed.toFixed(1)}s`);
|
||||||
if (opts.source) footer.push(opts.source);
|
if (opts.source) footer.push(opts.source);
|
||||||
|
if (opts.footerExtra) footer.push(opts.footerExtra);
|
||||||
console.log(chalk.dim(footer.join(' · ')));
|
console.log(chalk.dim(footer.join(' · ')));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface CliCommand {
|
|||||||
pipeline?: Record<string, unknown>[];
|
pipeline?: Record<string, unknown>[];
|
||||||
timeoutSeconds?: number;
|
timeoutSeconds?: number;
|
||||||
source?: string;
|
source?: string;
|
||||||
|
footerExtra?: (kwargs: Record<string, any>) => string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal extension for lazy-loaded TS modules (not exposed in public API) */
|
/** 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,
|
func: opts.func,
|
||||||
pipeline: opts.pipeline,
|
pipeline: opts.pipeline,
|
||||||
timeoutSeconds: opts.timeoutSeconds,
|
timeoutSeconds: opts.timeoutSeconds,
|
||||||
|
footerExtra: opts.footerExtra,
|
||||||
};
|
};
|
||||||
|
|
||||||
const key = fullName(cmd);
|
const key = fullName(cmd);
|
||||||
|
|||||||
Reference in new issue
Block a user