From dea524ea836ee69acaa20c95127a95efc788bc3d Mon Sep 17 00:00:00 2001 From: k1tyoodev Date: Fri, 20 Mar 2026 11:03:39 +0800 Subject: [PATCH 1/7] feat(hf): add top command for hf papers (daily, weekly, monthly) --- src/clis/hf/top.ts | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/clis/hf/top.ts diff --git a/src/clis/hf/top.ts b/src/clis/hf/top.ts new file mode 100644 index 0000000..be43419 --- /dev/null +++ b/src/clis/hf/top.ts @@ -0,0 +1,88 @@ +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 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.'; +} + +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: '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' }, + ], + func: async (_page, kwargs) => { + const period = String(kwargs.period ?? 'daily'); + 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 sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0)); + return sorted.slice(0, Number(kwargs.limit)).map((item, i) => ({ + rank: i + 1, + title: 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)); + return sorted.slice(0, Number(kwargs.limit)).map((item, i) => ({ + rank: i + 1, + title: item.title ?? '', + upvotes: item.paper?.upvotes ?? 0, + comments: item.numComments ?? 0, + authors: formatAuthors(item.paper?.authors ?? []), + })); + }, +}); From f58c6da2bf5e56a3fb0ed9c22445caf70988bff0 Mon Sep 17 00:00:00 2001 From: k1tyoodev Date: Fri, 20 Mar 2026 11:04:02 +0800 Subject: [PATCH 2/7] 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. --- src/cli.ts | 3 +- src/clis/hf/top.ts | 44 +++++++++++ src/main.ts | 191 ++++++++++++++++++++++++++++++++++++++++++++- src/output.ts | 2 + src/registry.ts | 2 + 5 files changed, 237 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 92ee5fd..7fc3a4e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -166,7 +166,8 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void { 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}`)); diff --git a/src/clis/hf/top.ts b/src/clis/hf/top.ts index be43419..a8ca7bc 100644 --- a/src/clis/hf/top.ts +++ b/src/clis/hf/top.ts @@ -29,6 +29,29 @@ function formatAuthors(authors: PaperAuthor[], max = 3): string { 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', @@ -41,6 +64,12 @@ cli({ { 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 endpoint = process.env.HF_ENDPOINT?.replace(/\/+$/, '') || 'https://huggingface.co'; @@ -55,6 +84,21 @@ cli({ 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)); return sorted.slice(0, Number(kwargs.limit)).map((item, i) => ({ rank: i + 1, diff --git a/src/main.ts b/src/main.ts index d4c5e58..a59db39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,9 +6,16 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { discoverClis } from './engine.js'; -import { getCompletions } from './completion.js'; -import { runCli } from './cli.js'; +import { Command } from 'commander'; +import chalk from 'chalk'; +import { discoverClis, executeCommand } from './engine.js'; +import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js'; +import { render as renderOutput } from './output.js'; +import { BrowserBridge } from './browser/index.js'; +import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js'; +import { PKG_VERSION } from './version.js'; +import { getCompletions, printCompletionScript } from './completion.js'; +import { CliError } from './errors.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -38,4 +45,180 @@ if (getCompIdx !== -1) { process.exit(0); } -runCli(BUILTIN_CLIS, USER_CLIS); +const program = new Command(); +program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION); + +// ── Built-in commands ────────────────────────────────────────────────────── + +program.command('list').description('List all available CLI commands').option('-f, --format ', 'Output format: table, json, yaml, md, csv', 'table').option('--json', 'JSON output (deprecated)') + .action((opts) => { + const registry = getRegistry(); + const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b))); + const rows = commands.map(c => ({ + command: fullName(c), + site: c.site, + name: c.name, + description: c.description, + strategy: strategyLabel(c), + browser: c.browser, + args: c.args.map(a => a.name).join(', '), + })); + const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format; + if (fmt !== 'table') { + renderOutput(rows, { + fmt, + columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'], + title: 'opencli/list', + source: 'opencli list', + }); + return; + } + const sites = new Map(); + for (const cmd of commands) { const g = sites.get(cmd.site) ?? []; g.push(cmd); sites.set(cmd.site, g); } + console.log(); console.log(chalk.bold(' opencli') + chalk.dim(' — available commands')); console.log(); + for (const [site, cmds] of sites) { + console.log(chalk.bold.cyan(` ${site}`)); + for (const cmd of cmds) { const tag = strategyLabel(cmd) === 'public' ? chalk.green('[public]') : chalk.yellow(`[${strategyLabel(cmd)}]`); console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`); } + console.log(); + } + console.log(chalk.dim(` ${commands.length} commands across ${sites.size} sites`)); console.log(); + }); + +program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name') + .action(async (target) => { + const { validateClisWithTarget, renderValidationReport } = await import('./validate.js'); + console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target))); + }); + +program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false) + .action(async (target, opts) => { + const { verifyClis, renderVerifyReport } = await import('./verify.js'); + const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke }); + console.log(renderVerifyReport(r)); + process.exitCode = r.ok ? 0 : 1; + }); + +program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('').option('--site ').option('--goal ').option('--wait ', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click ', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")') + .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserBridge, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); }); + +program.command('synthesize').description('Synthesize CLIs from explore').argument('').option('--top ', '', '3') + .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); }); + +program.command('generate').description('One-shot: explore → synthesize → register').argument('').option('--goal ').option('--site ') + .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: BrowserBridge, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; }); + +program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('').option('--site ') + .action(async (url, opts) => { + const { cascadeProbe, renderCascadeResult } = await import('./cascade.js'); + const result = await browserSession(BrowserBridge, async (page) => { + // Navigate to the site first for cookie context + try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {} + return cascadeProbe(page, url); + }); + console.log(renderCascadeResult(result)); + }); + +program.command('doctor') + .description('Diagnose opencli browser bridge connectivity') + .option('--live', 'Test browser connectivity (requires Chrome running)', false) + .action(async (opts) => { + const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js'); + const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION }); + console.log(renderBrowserDoctorReport(report)); + }); + +program.command('setup') + .description('Interactive setup: verify browser bridge connectivity') + .action(async () => { + const { runSetup } = await import('./setup.js'); + await runSetup({ cliVersion: PKG_VERSION }); + }); + +program.command('completion') + .description('Output shell completion script') + .argument('', 'Shell type: bash, zsh, or fish') + .action((shell) => { + printCompletionScript(shell); + }); + +// ── Dynamic site commands ────────────────────────────────────────────────── + +const registry = getRegistry(); +const siteGroups = new Map(); + +for (const [, cmd] of registry) { + let siteCmd = siteGroups.get(cmd.site); + if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); } + const subCmd = siteCmd.command(cmd.name).description(cmd.description); + + // Register positional args first, then named options + const positionalArgs: typeof cmd.args = []; + for (const arg of cmd.args) { + if (arg.positional) { + const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`; + subCmd.argument(bracket, arg.help ?? ''); + positionalArgs.push(arg); + } else { + const flag = arg.required ? `--${arg.name} ` : `--${arg.name} [value]`; + if (arg.required) subCmd.requiredOption(flag, arg.help ?? ''); + else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default)); + else subCmd.option(flag, arg.help ?? ''); + } + } + subCmd.option('-f, --format ', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false); + + subCmd.action(async (...actionArgs: any[]) => { + // Commander passes positional args first, then options object, then the Command + const actionOpts = actionArgs[positionalArgs.length] ?? {}; + const startTime = Date.now(); + const kwargs: Record = {}; + + // Collect positional args + for (let i = 0; i < positionalArgs.length; i++) { + const arg = positionalArgs[i]; + const v = actionArgs[i]; + if (v !== undefined) kwargs[arg.name] = v; + } + + // Collect named options + for (const arg of cmd.args) { + if (arg.positional) continue; + const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase()); + const v = actionOpts[arg.name] ?? actionOpts[camelName]; + if (v !== undefined) kwargs[arg.name] = v; + } + + try { + if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1'; + let result: any; + if (cmd.browser) { + result = await browserSession(BrowserBridge, async (page) => { + // Cookie/header strategies require same-origin context for credentialed fetch. + // In CDP mode the active tab may be on an unrelated domain, causing CORS failures. + // Navigate to the command's domain first (mirrors cascade command behavior). + 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.`)); + } + 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}`)); + if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`)); + } else if (actionOpts.verbose && err.stack) { + console.error(chalk.red(err.stack)); + } else { + console.error(chalk.red(`Error: ${err.message ?? err}`)); + } + process.exitCode = 1; + } + }); +} + +program.parse(); diff --git a/src/output.ts b/src/output.ts index d2cea78..2daf74c 100644 --- a/src/output.ts +++ b/src/output.ts @@ -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(' · '))); } diff --git a/src/registry.ts b/src/registry.ts index 894119d..ed50778 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -35,6 +35,7 @@ export interface CliCommand { pipeline?: Record[]; timeoutSeconds?: number; source?: string; + footerExtra?: (kwargs: Record) => 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); From 25e5285ee73f5904070079268bc27ded07c57400 Mon Sep 17 00:00:00 2001 From: k1tyoodev Date: Fri, 20 Mar 2026 11:04:21 +0800 Subject: [PATCH 3/7] fix: truncate long paper titles --- src/clis/hf/top.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/clis/hf/top.ts b/src/clis/hf/top.ts index a8ca7bc..952ec42 100644 --- a/src/clis/hf/top.ts +++ b/src/clis/hf/top.ts @@ -23,6 +23,10 @@ interface PeriodPaper { 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(', '); @@ -102,7 +106,7 @@ cli({ const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0)); return sorted.slice(0, Number(kwargs.limit)).map((item, i) => ({ rank: i + 1, - title: item.title ?? '', + title: truncate(item.title ?? ''), upvotes: item.upvotes ?? 0, authors: formatAuthors(item.authors ?? []), })); From 4bb764e5dd95083d59a3c882098c09c8e5dd05f7 Mon Sep 17 00:00:00 2001 From: k1tyoodev Date: Fri, 20 Mar 2026 11:04:31 +0800 Subject: [PATCH 4/7] refactor(hf): remove comments column for consistent output --- src/clis/hf/top.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/clis/hf/top.ts b/src/clis/hf/top.ts index 952ec42..6d05436 100644 --- a/src/clis/hf/top.ts +++ b/src/clis/hf/top.ts @@ -129,7 +129,6 @@ cli({ rank: i + 1, title: item.title ?? '', upvotes: item.paper?.upvotes ?? 0, - comments: item.numComments ?? 0, authors: formatAuthors(item.paper?.authors ?? []), })); }, From 0e4a9134fba5dd50e70f230e13de5b0847003fc5 Mon Sep 17 00:00:00 2001 From: k1tyoodev Date: Fri, 20 Mar 2026 11:05:01 +0800 Subject: [PATCH 5/7] feat(hf): add --all flag to return all papers --- src/clis/hf/top.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/clis/hf/top.ts b/src/clis/hf/top.ts index 6d05436..68dc8d9 100644 --- a/src/clis/hf/top.ts +++ b/src/clis/hf/top.ts @@ -65,6 +65,7 @@ cli({ 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' }, ], @@ -76,6 +77,7 @@ cli({ }, 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') { @@ -104,7 +106,8 @@ cli({ } } const sorted = [...data].sort((a, b) => (b.upvotes ?? 0) - (a.upvotes ?? 0)); - return sorted.slice(0, Number(kwargs.limit)).map((item, i) => ({ + const items = all ? sorted : sorted.slice(0, Number(kwargs.limit)); + return items.map((item, i) => ({ rank: i + 1, title: truncate(item.title ?? ''), upvotes: item.upvotes ?? 0, @@ -125,7 +128,8 @@ cli({ 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)); - return sorted.slice(0, Number(kwargs.limit)).map((item, i) => ({ + const items = all ? sorted : sorted.slice(0, Number(kwargs.limit)); + return items.map((item, i) => ({ rank: i + 1, title: item.title ?? '', upvotes: item.paper?.upvotes ?? 0, From 8d30605f5b739acfd412f3273940ea600c523765 Mon Sep 17 00:00:00 2001 From: k1tyoodev Date: Fri, 20 Mar 2026 11:06:07 +0800 Subject: [PATCH 6/7] feat(hf): add paper id column to output --- src/clis/hf/top.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/clis/hf/top.ts b/src/clis/hf/top.ts index 68dc8d9..b912b41 100644 --- a/src/clis/hf/top.ts +++ b/src/clis/hf/top.ts @@ -109,6 +109,7 @@ cli({ 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 ?? []), @@ -131,7 +132,8 @@ cli({ const items = all ? sorted : sorted.slice(0, Number(kwargs.limit)); return items.map((item, i) => ({ rank: i + 1, - title: item.title ?? '', + id: item.paper?.id ?? '', + title: truncate(item.title ?? ''), upvotes: item.paper?.upvotes ?? 0, authors: formatAuthors(item.paper?.authors ?? []), })); From 527e8da05ec38c125f54af2e11344b89ed7ebcc6 Mon Sep 17 00:00:00 2001 From: jackwener Date: Fri, 20 Mar 2026 14:17:40 +0800 Subject: [PATCH 7/7] fix: restore main.ts as bootstrap, sync footerExtra + CDPBridge + domain pre-nav to cli.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/cli.ts | 6 +- src/main.ts | 191 ++-------------------------------------------------- 2 files changed, 9 insertions(+), 188 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 7fc3a4e..9cc6f8d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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'; @@ -160,6 +160,10 @@ 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); } diff --git a/src/main.ts b/src/main.ts index a59db39..d4c5e58 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,16 +6,9 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { Command } from 'commander'; -import chalk from 'chalk'; -import { discoverClis, executeCommand } from './engine.js'; -import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js'; -import { render as renderOutput } from './output.js'; -import { BrowserBridge } from './browser/index.js'; -import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js'; -import { PKG_VERSION } from './version.js'; -import { getCompletions, printCompletionScript } from './completion.js'; -import { CliError } from './errors.js'; +import { discoverClis } from './engine.js'; +import { getCompletions } from './completion.js'; +import { runCli } from './cli.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -45,180 +38,4 @@ if (getCompIdx !== -1) { process.exit(0); } -const program = new Command(); -program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION); - -// ── Built-in commands ────────────────────────────────────────────────────── - -program.command('list').description('List all available CLI commands').option('-f, --format ', 'Output format: table, json, yaml, md, csv', 'table').option('--json', 'JSON output (deprecated)') - .action((opts) => { - const registry = getRegistry(); - const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b))); - const rows = commands.map(c => ({ - command: fullName(c), - site: c.site, - name: c.name, - description: c.description, - strategy: strategyLabel(c), - browser: c.browser, - args: c.args.map(a => a.name).join(', '), - })); - const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format; - if (fmt !== 'table') { - renderOutput(rows, { - fmt, - columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'], - title: 'opencli/list', - source: 'opencli list', - }); - return; - } - const sites = new Map(); - for (const cmd of commands) { const g = sites.get(cmd.site) ?? []; g.push(cmd); sites.set(cmd.site, g); } - console.log(); console.log(chalk.bold(' opencli') + chalk.dim(' — available commands')); console.log(); - for (const [site, cmds] of sites) { - console.log(chalk.bold.cyan(` ${site}`)); - for (const cmd of cmds) { const tag = strategyLabel(cmd) === 'public' ? chalk.green('[public]') : chalk.yellow(`[${strategyLabel(cmd)}]`); console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`); } - console.log(); - } - console.log(chalk.dim(` ${commands.length} commands across ${sites.size} sites`)); console.log(); - }); - -program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name') - .action(async (target) => { - const { validateClisWithTarget, renderValidationReport } = await import('./validate.js'); - console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target))); - }); - -program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false) - .action(async (target, opts) => { - const { verifyClis, renderVerifyReport } = await import('./verify.js'); - const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke }); - console.log(renderVerifyReport(r)); - process.exitCode = r.ok ? 0 : 1; - }); - -program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('').option('--site ').option('--goal ').option('--wait ', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click ', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")') - .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserBridge, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); }); - -program.command('synthesize').description('Synthesize CLIs from explore').argument('').option('--top ', '', '3') - .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); }); - -program.command('generate').description('One-shot: explore → synthesize → register').argument('').option('--goal ').option('--site ') - .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: BrowserBridge, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; }); - -program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('').option('--site ') - .action(async (url, opts) => { - const { cascadeProbe, renderCascadeResult } = await import('./cascade.js'); - const result = await browserSession(BrowserBridge, async (page) => { - // Navigate to the site first for cookie context - try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {} - return cascadeProbe(page, url); - }); - console.log(renderCascadeResult(result)); - }); - -program.command('doctor') - .description('Diagnose opencli browser bridge connectivity') - .option('--live', 'Test browser connectivity (requires Chrome running)', false) - .action(async (opts) => { - const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js'); - const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION }); - console.log(renderBrowserDoctorReport(report)); - }); - -program.command('setup') - .description('Interactive setup: verify browser bridge connectivity') - .action(async () => { - const { runSetup } = await import('./setup.js'); - await runSetup({ cliVersion: PKG_VERSION }); - }); - -program.command('completion') - .description('Output shell completion script') - .argument('', 'Shell type: bash, zsh, or fish') - .action((shell) => { - printCompletionScript(shell); - }); - -// ── Dynamic site commands ────────────────────────────────────────────────── - -const registry = getRegistry(); -const siteGroups = new Map(); - -for (const [, cmd] of registry) { - let siteCmd = siteGroups.get(cmd.site); - if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); } - const subCmd = siteCmd.command(cmd.name).description(cmd.description); - - // Register positional args first, then named options - const positionalArgs: typeof cmd.args = []; - for (const arg of cmd.args) { - if (arg.positional) { - const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`; - subCmd.argument(bracket, arg.help ?? ''); - positionalArgs.push(arg); - } else { - const flag = arg.required ? `--${arg.name} ` : `--${arg.name} [value]`; - if (arg.required) subCmd.requiredOption(flag, arg.help ?? ''); - else if (arg.default != null) subCmd.option(flag, arg.help ?? '', String(arg.default)); - else subCmd.option(flag, arg.help ?? ''); - } - } - subCmd.option('-f, --format ', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false); - - subCmd.action(async (...actionArgs: any[]) => { - // Commander passes positional args first, then options object, then the Command - const actionOpts = actionArgs[positionalArgs.length] ?? {}; - const startTime = Date.now(); - const kwargs: Record = {}; - - // Collect positional args - for (let i = 0; i < positionalArgs.length; i++) { - const arg = positionalArgs[i]; - const v = actionArgs[i]; - if (v !== undefined) kwargs[arg.name] = v; - } - - // Collect named options - for (const arg of cmd.args) { - if (arg.positional) continue; - const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase()); - const v = actionOpts[arg.name] ?? actionOpts[camelName]; - if (v !== undefined) kwargs[arg.name] = v; - } - - try { - if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1'; - let result: any; - if (cmd.browser) { - result = await browserSession(BrowserBridge, async (page) => { - // Cookie/header strategies require same-origin context for credentialed fetch. - // In CDP mode the active tab may be on an unrelated domain, causing CORS failures. - // Navigate to the command's domain first (mirrors cascade command behavior). - 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.`)); - } - 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}`)); - if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`)); - } else if (actionOpts.verbose && err.stack) { - console.error(chalk.red(err.stack)); - } else { - console.error(chalk.red(`Error: ${err.message ?? err}`)); - } - process.exitCode = 1; - } - }); -} - -program.parse(); +runCli(BUILTIN_CLIS, USER_CLIS);