From 9587956650d3d6df2f40e1721316cfb239d21cda Mon Sep 17 00:00:00 2001 From: fanhuanjie Date: Sun, 5 Jul 2026 20:42:02 +0800 Subject: [PATCH] feat(i18n): add arxiv skill description translations Add localized descriptions for the arxiv skill across all supported languages (en, es, fr, ja, ru, zh, zht) and register it in the builtin skill set. --- packages/opencode/src/cli/cmd/tui/i18n/en.ts | 1 + packages/opencode/src/cli/cmd/tui/i18n/es.ts | 1 + packages/opencode/src/cli/cmd/tui/i18n/fr.ts | 1 + packages/opencode/src/cli/cmd/tui/i18n/ja.ts | 1 + packages/opencode/src/cli/cmd/tui/i18n/ru.ts | 1 + .../opencode/src/cli/cmd/tui/i18n/skill.ts | 1 + packages/opencode/src/cli/cmd/tui/i18n/zh.ts | 1 + packages/opencode/src/cli/cmd/tui/i18n/zht.ts | 1 + .../src/skill/builtin/.bundle/arxiv/SKILL.md | 112 ++++++ .../builtin/.bundle/arxiv/scripts/arxiv.py | 319 ++++++++++++++++++ 10 files changed, 439 insertions(+) create mode 100644 packages/opencode/src/skill/builtin/.bundle/arxiv/SKILL.md create mode 100644 packages/opencode/src/skill/builtin/.bundle/arxiv/scripts/arxiv.py diff --git a/packages/opencode/src/cli/cmd/tui/i18n/en.ts b/packages/opencode/src/cli/cmd/tui/i18n/en.ts index b83df9fe4..dd343c70f 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/en.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/en.ts @@ -206,6 +206,7 @@ export const dict: Record = { "tui.skill.frontend-design.description": "Guidance for distinctive, intentional visual UI design", "tui.skill.loop.description": "Schedule a prompt to run on a recurring interval", "tui.skill.html-to-video-pipeline.description": "Short-video magic — make short videos with HTML", + "tui.skill.arxiv.description": "Search, cite, download, and track arXiv papers", // Language switching "tui.command.language.switch.title": "Switch language", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/es.ts b/packages/opencode/src/cli/cmd/tui/i18n/es.ts index 3e12ae93c..510e45a53 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/es.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/es.ts @@ -282,6 +282,7 @@ export const dict = { "tui.skill.frontend-design.description": "Guía para un diseño visual de UI distintivo e intencional", "tui.skill.loop.description": "Programar un prompt para ejecutarse en un intervalo recurrente", "tui.skill.html-to-video-pipeline.description": "El arma definitiva para vídeos cortos — crea vídeos cortos con HTML", + "tui.skill.arxiv.description": "Busca, cita, descarga y sigue artículos de arXiv", // Language switching "tui.command.language.switch.title": "Cambiar idioma", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/fr.ts b/packages/opencode/src/cli/cmd/tui/i18n/fr.ts index 62a6d48e5..ef3558282 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/fr.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/fr.ts @@ -270,6 +270,7 @@ export const dict = { "tui.skill.frontend-design.description": "Conseils pour un design d'interface visuel distinctif et intentionnel", "tui.skill.loop.description": "Planifier l'exécution récurrente d'un prompt", "tui.skill.html-to-video-pipeline.description": "L'arme ultime pour vidéos courtes — créez des vidéos courtes avec du HTML", + "tui.skill.arxiv.description": "Rechercher, citer, télécharger et suivre des articles arXiv", // Language switching "tui.command.language.switch.title": "Changer de langue", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/ja.ts b/packages/opencode/src/cli/cmd/tui/i18n/ja.ts index e10750925..01fea4a22 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/ja.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/ja.ts @@ -219,6 +219,7 @@ export const dict = { "tui.skill.frontend-design.description": "個性的で意図的な UI ビジュアルデザインのガイド", "tui.skill.loop.description": "プロンプトを一定間隔で繰り返し実行するようスケジュール", "tui.skill.html-to-video-pipeline.description": "ショート動画の神ツール - HTML でショート動画を制作", + "tui.skill.arxiv.description": "arXiv 論文の検索・引用・ダウンロード・追跡", // Language switching "tui.command.language.switch.title": "言語を切り替え", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/ru.ts b/packages/opencode/src/cli/cmd/tui/i18n/ru.ts index 7b3db7184..110a77da7 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/ru.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/ru.ts @@ -285,6 +285,7 @@ export const dict = { "tui.skill.frontend-design.description": "Руководство по выразительному, осмысленному визуальному дизайну UI", "tui.skill.loop.description": "Запланировать запуск промпта с периодичностью", "tui.skill.html-to-video-pipeline.description": "Магический инструмент для коротких видео — создавайте короткие видео с помощью HTML", + "tui.skill.arxiv.description": "Поиск, цитирование, загрузка и отслеживание статей arXiv", // Language switching "tui.command.language.switch.title": "Сменить язык", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/skill.ts b/packages/opencode/src/cli/cmd/tui/i18n/skill.ts index 4e4c5c724..7d87c50f8 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/skill.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/skill.ts @@ -8,6 +8,7 @@ const BUILTIN = new Set([ "frontend-design", "loop", "html-to-video-pipeline", + "arxiv", ]) export function skillDescription( diff --git a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts index f106d0844..c1790e4b9 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/zh.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/zh.ts @@ -195,6 +195,7 @@ export const dict = { "tui.skill.frontend-design.description": "具备鲜明主张的前端视觉设计指南", "tui.skill.loop.description": "按固定周期循环运行提示词", "tui.skill.html-to-video-pipeline.description": "短视频神器 - 利用 HTML 制作短视频", + "tui.skill.arxiv.description": "搜索、引用、下载与追踪 arXiv 论文", // Language switching "tui.command.language.switch.title": "切换语言", diff --git a/packages/opencode/src/cli/cmd/tui/i18n/zht.ts b/packages/opencode/src/cli/cmd/tui/i18n/zht.ts index 9551813b2..47459139d 100644 --- a/packages/opencode/src/cli/cmd/tui/i18n/zht.ts +++ b/packages/opencode/src/cli/cmd/tui/i18n/zht.ts @@ -195,6 +195,7 @@ export const dict = { "tui.skill.frontend-design.description": "具備鮮明主張的前端視覺設計指引", "tui.skill.loop.description": "依固定週期循環執行提示詞", "tui.skill.html-to-video-pipeline.description": "短影片神器 - 利用 HTML 製作短影片", + "tui.skill.arxiv.description": "搜尋、引用、下載與追蹤 arXiv 論文", // Language switching "tui.command.language.switch.title": "切換語言", diff --git a/packages/opencode/src/skill/builtin/.bundle/arxiv/SKILL.md b/packages/opencode/src/skill/builtin/.bundle/arxiv/SKILL.md new file mode 100644 index 000000000..efb5cd77a --- /dev/null +++ b/packages/opencode/src/skill/builtin/.bundle/arxiv/SKILL.md @@ -0,0 +1,112 @@ +--- +name: arxiv +description: "Use this skill whenever the user wants to find, read, cite, track, download, or analyze academic papers on arXiv. That includes: searching papers by topic, author, category, or arXiv ID; fetching abstracts or full metadata; generating BibTeX citations; downloading PDFs; listing the latest submissions in a field (e.g. cs.AI daily digest); checking a paper's citation impact; finding who cites a paper, what it references, or related-paper recommendations. Trigger on mentions of 'arXiv', an arXiv ID (e.g. 2601.02780 or hep-th/0601001), an arxiv.org URL, 'paper search', 'literature review', 'find papers about X', 'cite this paper', or 'what's new in cs.LG'." +version: 2.0.0 +license: MIT +platforms: [linux, macos, windows] +--- + +# arXiv Research + +Search, read, cite, and analyze academic papers using the free arXiv API and Semantic Scholar API. No API keys, no dependencies — the bundled script uses only the Python stdlib. + +## Quick Start: the `arxiv.py` script + +Prefer `scripts/arxiv.py` over raw `curl` — it handles Atom XML parsing, versioned IDs, withdrawn-paper detection, and produces clean readable (or `--json`) output. + +| Goal | Command | +|------|---------| +| Search papers | `python scripts/arxiv.py search "GRPO reinforcement learning" --max 10 --sort date` | +| Filtered search | `python scripts/arxiv.py search "attention" --author vaswani --category cs.CL` | +| Full metadata + abstract | `python scripts/arxiv.py get 2601.02780,1706.03762` | +| BibTeX citation | `python scripts/arxiv.py bibtex 2601.02780` | +| Download PDF | `python scripts/arxiv.py download 2601.02780 --dest ./papers` | +| Latest in a category | `python scripts/arxiv.py new cs.CL --max 10` | +| Who cites this paper | `python scripts/arxiv.py cites 2601.02780 --max 20` | +| What this paper cites | `python scripts/arxiv.py refs 2601.02780` | +| Related-paper recommendations | `python scripts/arxiv.py similar 2601.02780` | +| Machine-readable output | append `--json` to any command | + +Common flags: `--max N` (result count), `--sort relevance|date|updated`, `--start N` (pagination offset, search only), `--json`. + +## Reading Paper Content + +After finding a paper, read it with the `webfetch` tool: + +- Abstract page (fast, metadata + abstract): `https://arxiv.org/abs/2601.02780` +- Full paper as HTML (best for reading, when available): `https://arxiv.org/html/2601.02780` +- PDF: `https://arxiv.org/pdf/2601.02780` + +If HTML is unavailable and the PDF must be processed locally, `download` it first, then use a PDF-processing skill. + +## Recommended Research Workflows + +**Literature review on a topic** +1. `search "topic" --sort date --max 15` — recent work +2. `search "topic" --max 15` — seminal work (relevance-sorted) +3. Cross-check impact: `python scripts/arxiv.py get ID` then `cites ID --max 5` for citation counts +4. Read the top candidates via `webfetch` on the abs/html URLs +5. `bibtex ID1,ID2,...` for the papers you keep + +**Deep-dive a single paper** +1. `get ID` — full abstract, versions, journal ref, DOI +2. `refs ID` — what it builds on +3. `cites ID` — follow-up work (sorted by citation count) +4. `similar ID` — related papers you might have missed +5. `webfetch` the HTML/PDF for full text + +**Stay current in a field** +- `new cs.AI --max 20` — latest submissions in a category +- Category taxonomy: https://arxiv.org/category_taxonomy — common ones: `cs.AI`, `cs.CL` (NLP), `cs.CV`, `cs.LG`, `cs.CR`, `stat.ML`, `math.OC` + +## Raw API Reference (when the script isn't enough) + +The script covers most needs; use the raw APIs for advanced queries. + +### arXiv API (Atom XML) + +```bash +curl -s "https://export.arxiv.org/api/query?search_query=all:transformer&max_results=5" +``` + +Field prefixes: `all:` (everything), `ti:` (title), `au:` (author), `abs:` (abstract), `cat:` (category), `co:` (comment, e.g. `co:accepted+NeurIPS`). + +Boolean syntax (URL-encode spaces as `+`): + +``` +all:GPT+OR+all:BERT # OR +all:language+model+ANDNOT+all:vision # AND NOT +ti:"chain+of+thought" # exact phrase +au:hinton+AND+cat:cs.LG # combined +``` + +Parameters: `sortBy` (`relevance`|`lastUpdatedDate`|`submittedDate`), `sortOrder`, `start`, `max_results`, `id_list` (comma-separated IDs). + +### Semantic Scholar API (JSON) + +arXiv has no citation data — Semantic Scholar fills that gap (free, ~1 req/sec unauthenticated). + +```bash +# Paper details with citation counts +curl -s "https://api.semanticscholar.org/graph/v1/paper/arXiv:2601.02780?fields=title,citationCount,influentialCitationCount,tldr" + +# Author profile +curl -s "https://api.semanticscholar.org/graph/v1/author/search?query=Yann+LeCun&fields=name,hIndex,citationCount,paperCount" + +# Keyword search returning JSON (alternative to arXiv search) +curl -s "https://api.semanticscholar.org/graph/v1/paper/search?query=GRPO&limit=5&fields=title,year,citationCount,externalIds" +``` + +Useful fields: `title`, `authors`, `year`, `abstract`, `tldr` (AI summary), `citationCount`, `influentialCitationCount`, `isOpenAccess`, `openAccessPdf`, `fieldsOfStudy`, `publicationVenue`, `externalIds` (arXiv ID, DOI). + +## Important Details + +**Rate limits** — arXiv: ~1 request / 3 seconds; Semantic Scholar: ~1 request / second. Space out consecutive calls; the script exits with a clear message on HTTP 429. + +**ID formats** — new style `2402.03300`, old style `hep-th/0601001`. Both work everywhere. + +**Versioning** — `arxiv.org/abs/2601.02780` resolves to the latest version; `...v2` is a specific immutable version. `bibtex` and `download` preserve the version suffix so citations match the content you actually read (later versions can change substantially). + +**Withdrawn papers** — the script flags entries whose abstract indicates withdrawal/retraction with `[WITHDRAWN]`. Don't cite these without noting the status. + +**Listings caveat** — `new CATEGORY` sorts by submission date via the search API; the official "new today" listing at `https://arxiv.org/list/cs.AI/new` may group slightly differently (cross-lists, replacements). diff --git a/packages/opencode/src/skill/builtin/.bundle/arxiv/scripts/arxiv.py b/packages/opencode/src/skill/builtin/.bundle/arxiv/scripts/arxiv.py new file mode 100644 index 000000000..cf20387b0 --- /dev/null +++ b/packages/opencode/src/skill/builtin/.bundle/arxiv/scripts/arxiv.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +"""arXiv research CLI — search, read, cite, download, and analyze papers. + +Stdlib only. No API keys required. + +Commands: + search QUERY Search arXiv (combine with --author/--category) + get ID[,ID...] Full metadata + abstract for specific papers + bibtex ID[,ID...] BibTeX entries for papers + download ID Download the PDF (--dest DIR, default cwd) + new CATEGORY Latest submissions in a category (e.g. cs.AI) + cites ID Papers citing this one (Semantic Scholar) + refs ID Papers this one references (Semantic Scholar) + similar ID Recommended related papers (Semantic Scholar) + +Examples: + python arxiv.py search "MiMo Technical Report" --max 10 --sort date + python arxiv.py search "attention" --author vaswani --category cs.CL + python arxiv.py get 2601.02780,1706.03762 + python arxiv.py bibtex 2601.02780 + python arxiv.py download 2601.02780 --dest ./papers + python arxiv.py new cs.CL --max 10 + python arxiv.py cites 2601.02780 --max 20 + python arxiv.py similar 2601.02780 + python arxiv.py search "diffusion models" --json + +Flags: + --max N Max results (default 5) + --start N Result offset for pagination (search only) + --sort MODE relevance | date | updated (search only) + --author NAME Filter by author (search only) + --category CAT Filter by category (search only) + --dest DIR Download directory (download only) + --json Emit JSON instead of readable text +""" +import argparse +import json +import os +import re +import sys +import urllib.error +import urllib.parse +import urllib.request +import xml.etree.ElementTree as ET + +ARXIV_API = "https://export.arxiv.org/api/query" +S2_API = "https://api.semanticscholar.org" +NS = { + "a": "http://www.w3.org/2005/Atom", + "arxiv": "http://arxiv.org/schemas/atom", + "os": "http://a9.com/-/spec/opensearch/1.1/", +} +UA = "MiMoCode-arxiv-skill/2.0" + + +def http_get(url, timeout=30): + req = urllib.request.Request(url, headers={"User-Agent": UA}) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + return resp.read() + except urllib.error.HTTPError as e: + if e.code == 429: + sys.exit(f"Rate limited by {urllib.parse.urlsplit(url).netloc} — wait a few seconds and retry.") + sys.exit(f"HTTP {e.code} fetching {url}") + except urllib.error.URLError as e: + sys.exit(f"Network error: {e.reason}") + + +def text_of(entry, tag): + node = entry.find(tag, NS) + return node.text.strip() if node is not None and node.text else "" + + +def parse_entry(entry): + raw_id = text_of(entry, "a:id") + full_id = raw_id.split("/abs/")[-1] if "/abs/" in raw_id else raw_id + base_id = re.sub(r"v\d+$", "", full_id) + summary = re.sub(r"\s+", " ", text_of(entry, "a:summary")) + primary = entry.find("arxiv:primary_category", NS) + doi = text_of(entry, "arxiv:doi") + comment = text_of(entry, "arxiv:comment") + journal = text_of(entry, "arxiv:journal_ref") + return { + "id": base_id, + "version": full_id[len(base_id):], + "title": re.sub(r"\s+", " ", text_of(entry, "a:title")), + "authors": [a.find("a:name", NS).text for a in entry.findall("a:author", NS)], + "published": text_of(entry, "a:published")[:10], + "updated": text_of(entry, "a:updated")[:10], + "abstract": summary, + "categories": [c.get("term") for c in entry.findall("a:category", NS)], + "primary_category": primary.get("term") if primary is not None else "", + "doi": doi, + "comment": comment, + "journal_ref": journal, + "abs_url": f"https://arxiv.org/abs/{base_id}", + "pdf_url": f"https://arxiv.org/pdf/{base_id}", + "html_url": f"https://arxiv.org/html/{base_id}", + "withdrawn": "withdrawn" in summary.lower()[:200] or "retracted" in summary.lower()[:200], + } + + +def fetch_entries(params): + url = ARXIV_API + "?" + urllib.parse.urlencode(params) + root = ET.fromstring(http_get(url)) + total = root.find("os:totalResults", NS) + entries = [parse_entry(e) for e in root.findall("a:entry", NS)] + return int(total.text) if total is not None else len(entries), entries + + +def print_paper(p, i=None, full=False): + prefix = f"{i}. " if i is not None else "" + flag = " [WITHDRAWN]" if p["withdrawn"] else "" + print(f"{prefix}{p['title']}{flag}") + print(f" ID: {p['id']}{p['version']} | Published: {p['published']} | Updated: {p['updated']}") + print(f" Authors: {', '.join(p['authors'])}") + print(f" Categories: {', '.join(p['categories'])}") + if p["journal_ref"]: + print(f" Journal: {p['journal_ref']}") + if p["comment"]: + print(f" Comment: {p['comment']}") + if p["doi"]: + print(f" DOI: {p['doi']}") + abstract = p["abstract"] if full else p["abstract"][:300] + ("..." if len(p["abstract"]) > 300 else "") + print(f" Abstract: {abstract}") + print(f" Links: {p['abs_url']} | {p['pdf_url']}") + print() + + +def emit(papers, args, total=None, full=False): + if args.json: + print(json.dumps({"total": total, "results": papers} if total is not None else papers, + ensure_ascii=False, indent=2)) + return + if not papers: + print("No results found.") + return + if total is not None: + print(f"Found {total} results (showing {len(papers)})\n") + for i, p in enumerate(papers, 1): + print_paper(p, i, full=full) + + +def cmd_search(args): + parts = [] + if args.query: + parts.append(f"all:{args.query}") + if args.author: + parts.append(f'au:"{args.author}"') + if args.category: + parts.append(f"cat:{args.category}") + if not parts: + sys.exit("Provide a query, --author, or --category.") + sort_map = {"relevance": "relevance", "date": "submittedDate", "updated": "lastUpdatedDate"} + total, papers = fetch_entries({ + "search_query": " AND ".join(parts), + "start": args.start, + "max_results": args.max, + "sortBy": sort_map[args.sort], + "sortOrder": "descending", + }) + emit(papers, args, total=total) + + +def cmd_get(args): + _, papers = fetch_entries({"id_list": args.ids, "max_results": 100}) + emit(papers, args, full=True) + + +def bibtex_key(p): + last = p["authors"][0].split()[-1].lower() if p["authors"] else "unknown" + return re.sub(r"[^a-z0-9]", "", last) + p["published"][:4] + p["id"].replace(".", "").replace("/", "") + + +def cmd_bibtex(args): + _, papers = fetch_entries({"id_list": args.ids, "max_results": 100}) + if not papers: + sys.exit("Paper not found.") + for p in papers: + eprint = p["id"] + p["version"] # keep version to prevent citation drift + lines = [ + f"@misc{{{bibtex_key(p)},", + f" title = {{{p['title']}}},", + f" author = {{{' and '.join(p['authors'])}}},", + f" year = {{{p['published'][:4]}}},", + f" eprint = {{{eprint}}},", + " archivePrefix = {arXiv},", + f" primaryClass = {{{p['primary_category']}}},", + f" url = {{https://arxiv.org/abs/{eprint}}},", + ] + if p["doi"]: + lines.append(f" doi = {{{p['doi']}}},") + lines.append("}") + print("\n".join(lines)) + print() + + +def cmd_download(args): + paper_id = args.ids.split(",")[0] + _, papers = fetch_entries({"id_list": paper_id}) + if not papers: + sys.exit(f"Paper {paper_id} not found.") + p = papers[0] + if p["withdrawn"]: + print(f"Warning: {p['id']} appears to be withdrawn.", file=sys.stderr) + os.makedirs(args.dest, exist_ok=True) + dest = os.path.join(args.dest, f"{p['id'].replace('/', '_')}{p['version']}.pdf") + data = http_get(f"https://arxiv.org/pdf/{p['id']}{p['version']}", timeout=120) + if not data.startswith(b"%PDF"): + sys.exit("Downloaded content is not a PDF (paper may lack a PDF version).") + with open(dest, "wb") as f: + f.write(data) + print(f"Saved: {dest} ({len(data) // 1024} KB)") + print(f"Title: {p['title']}") + + +def cmd_new(args): + total, papers = fetch_entries({ + "search_query": f"cat:{args.category}", + "max_results": args.max, + "sortBy": "submittedDate", + "sortOrder": "descending", + }) + emit(papers, args, total=total) + + +S2_FIELDS = "title,authors,year,citationCount,externalIds,abstract" + + +def s2_paper_row(p): + ext = p.get("externalIds") or {} + return { + "title": p.get("title"), + "authors": [a["name"] for a in p.get("authors") or []], + "year": p.get("year"), + "citations": p.get("citationCount"), + "arxiv_id": ext.get("ArXiv"), + "doi": ext.get("DOI"), + } + + +def print_s2(rows, args, header): + if args.json: + print(json.dumps(rows, ensure_ascii=False, indent=2)) + return + if not rows: + print("No results.") + return + print(f"{header}\n") + for i, r in enumerate(rows, 1): + arxiv = f" | arXiv:{r['arxiv_id']}" if r["arxiv_id"] else "" + print(f"{i}. {r['title']} ({r['year']})") + print(f" Authors: {', '.join(r['authors'][:6])}{' ...' if len(r['authors']) > 6 else ''}") + print(f" Citations: {r['citations']}{arxiv}") + print() + + +def cmd_s2_links(args, direction): + paper_id = args.ids.split(",")[0] + url = f"{S2_API}/graph/v1/paper/arXiv:{paper_id}/{direction}?fields={S2_FIELDS}&limit={args.max}" + data = json.loads(http_get(url)) + key = "citingPaper" if direction == "citations" else "citedPaper" + rows = [s2_paper_row(item[key]) for item in data.get("data", [])] + rows.sort(key=lambda r: r["citations"] or 0, reverse=True) + label = "Papers citing" if direction == "citations" else "References of" + print_s2(rows, args, f"{label} arXiv:{paper_id} (sorted by citations)") + + +def cmd_similar(args): + paper_id = args.ids.split(",")[0] + url = f"{S2_API}/recommendations/v1/papers/forpaper/arXiv:{paper_id}?fields={S2_FIELDS}&limit={args.max}" + data = json.loads(http_get(url)) + rows = [s2_paper_row(p) for p in data.get("recommendedPapers", [])] + print_s2(rows, args, f"Papers similar to arXiv:{paper_id}") + + +def main(): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("command", nargs="?") + parser.add_argument("query", nargs="*") + parser.add_argument("--max", type=int, default=5) + parser.add_argument("--start", type=int, default=0) + parser.add_argument("--sort", choices=["relevance", "date", "updated"], default="relevance") + parser.add_argument("--author") + parser.add_argument("--category") + parser.add_argument("--dest", default=".") + parser.add_argument("--json", action="store_true") + parser.add_argument("-h", "--help", action="store_true") + args = parser.parse_args() + + if args.help or not args.command: + print(__doc__) + sys.exit(0) + + args.ids = ",".join(args.query) + args.query = " ".join(args.query) + + commands = { + "search": cmd_search, + "get": cmd_get, + "bibtex": cmd_bibtex, + "download": cmd_download, + "new": lambda a: cmd_new(argparse.Namespace(**{**vars(a), "category": a.query or a.category})), + "cites": lambda a: cmd_s2_links(a, "citations"), + "refs": lambda a: cmd_s2_links(a, "references"), + "similar": cmd_similar, + } + handler = commands.get(args.command) + if handler is None: + # bare query fallback: treat first arg as part of the search + args.query = (args.command + " " + args.query).strip() + handler = cmd_search + if args.command == "new" and not (args.query or args.category): + sys.exit("Provide a category, e.g.: python arxiv.py new cs.AI") + handler(args) + + +if __name__ == "__main__": + main()