Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -160,13 +160,18 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
if (cmd.browser) {
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) });
});
} 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}`));
Expand Down
141 changes: 141 additions & 0 deletions src/clis/hf/top.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions src/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface RenderOptions {
title?: string;
elapsed?: number;
source?: string;
footerExtra?: string;
}

export function render(data: unknown, opts: RenderOptions = {}): void {
Expand Down Expand Up @@ -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(' Β· ')));
}

Expand Down
2 changes: 2 additions & 0 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand Down Expand Up @@ -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);
Expand Down