top.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 import { CliError } from '@jackwener/opencli/errors'; 3 function truncate(str, max = 60) { 4 return str.length > max ? str.slice(0, max - 3) + '...' : str; 5 } 6 function formatAuthors(authors, max = 3) { 7 const names = authors.map((a) => a.name); 8 if (names.length <= max) 9 return names.join(', '); 10 return names.slice(0, max).join(', ') + ' et al.'; 11 } 12 const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 13 function getMonthRange() { 14 const now = new Date(); 15 return `${MONTH_ABBR[now.getUTCMonth()]} ${now.getUTCFullYear()}`; 16 } 17 function getWeekRange() { 18 const now = new Date(); 19 const day = now.getUTCDay(); // 0=Sun, 6=Sat 20 const daysToSat = day === 6 ? 0 : 6 - day; 21 const end = new Date(now); 22 end.setUTCDate(now.getUTCDate() + daysToSat); 23 const start = new Date(end); 24 start.setUTCDate(end.getUTCDate() - 6); 25 const sm = MONTH_ABBR[start.getUTCMonth()]; 26 const em = MONTH_ABBR[end.getUTCMonth()]; 27 const sd = start.getUTCDate(); 28 const ed = end.getUTCDate(); 29 return sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`; 30 } 31 cli({ 32 site: 'hf', 33 name: 'top', 34 description: 'Top upvoted Hugging Face papers', 35 domain: 'huggingface.co', 36 strategy: Strategy.PUBLIC, 37 browser: false, 38 args: [ 39 { name: 'limit', type: 'int', default: 20, help: 'Number of papers' }, 40 { name: 'all', type: 'bool', default: false, help: 'Return all papers (ignore limit)' }, 41 { name: 'date', type: 'str', required: false, help: 'Date (YYYY-MM-DD), defaults to most recent' }, 42 { name: 'period', type: 'str', default: 'daily', choices: ['daily', 'weekly', 'monthly'], help: 'Time period: daily, weekly, or monthly' }, 43 ], 44 footerExtra: (kwargs) => { 45 if (kwargs._footerDate) 46 return kwargs._footerDate; 47 if (kwargs.period === 'monthly') 48 return getMonthRange(); 49 if (kwargs.period === 'weekly') 50 return getWeekRange(); 51 return kwargs.date ?? new Date().toISOString().slice(0, 10); 52 }, 53 func: async (_page, kwargs) => { 54 const period = String(kwargs.period ?? 'daily'); 55 const all = Boolean(kwargs.all); 56 const endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co'; 57 if (period === 'weekly' || period === 'monthly') { 58 if (kwargs.date) { 59 throw new CliError('INVALID_ARG', `--date is not supported for ${period} period`, `Omit --date when using --period ${period}`); 60 } 61 const url = `${endpoint}/api/papers?period=${period}`; 62 const res = await fetch(url); 63 if (!res.ok) 64 throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later'); 65 const body = await res.json(); 66 if (!Array.isArray(body)) 67 throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check endpoint'); 68 const data = body; 69 const dates = data.map((d) => d.publishedAt).filter(Boolean).sort(); 70 if (dates.length > 0) { 71 if (period === 'monthly') { 72 const d = new Date(dates[0]); 73 kwargs._footerDate = `${MONTH_ABBR[d.getUTCMonth()]} ${d.getUTCFullYear()}`; 74 } 75 else { 76 const start = new Date(dates[0]); 77 const end = new Date(dates[dates.length - 1]); 78 const sm = MONTH_ABBR[start.getUTCMonth()]; 79 const em = MONTH_ABBR[end.getUTCMonth()]; 80 const sd = start.getUTCDate(); 81 const ed = end.getUTCDate(); 82 kwargs._footerDate = sm === em ? `${sm} ${sd}-${ed}` : `${sm} ${sd}-${em} ${ed}`; 83 } 84 } 85 const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0)); 86 const items = all ? sorted : sorted.slice(0, Number(kwargs.limit)); 87 return items.map((item, i) => ({ 88 rank: i + 1, 89 id: item.id ?? '', 90 title: truncate(item.title ?? ''), 91 upvotes: item.upvotes ?? 0, 92 authors: formatAuthors(item.authors ?? []), 93 })); 94 } 95 // daily 96 if (kwargs.date && !/^\d{4}-\d{2}-\d{2}$/.test(String(kwargs.date))) { 97 throw new CliError('INVALID_ARG', `Invalid date format: ${kwargs.date}`, 'Use YYYY-MM-DD'); 98 } 99 const url = kwargs.date 100 ? `${endpoint}/api/daily_papers?date=${kwargs.date}` 101 : `${endpoint}/api/daily_papers`; 102 const res = await fetch(url); 103 if (!res.ok) 104 throw new CliError('FETCH_ERROR', `HF API error: ${res.status} ${res.statusText}`, 'Check HF_ENDPOINT or try again later'); 105 const body = await res.json(); 106 if (!Array.isArray(body)) 107 throw new CliError('FETCH_ERROR', 'Unexpected HF API response', 'Check date format or endpoint'); 108 const data = body; 109 const sorted = [...data].sort((a, b) => (b.paper?.upvotes ?? 0) - (a.paper?.upvotes ?? 0)); 110 const items = all ? sorted : sorted.slice(0, Number(kwargs.limit)); 111 return items.map((item, i) => ({ 112 rank: i + 1, 113 id: item.paper?.id ?? '', 114 title: truncate(item.title ?? ''), 115 upvotes: item.paper?.upvotes ?? 0, 116 authors: formatAuthors(item.paper?.authors ?? []), 117 })); 118 }, 119 });