📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
---
|
||||
name: papers-skill
|
||||
description: "Skill for academic research workflows: search Semantic Scholar (200M+ papers), inspect citations, download arXiv PDFs, and extract PDF text. Bundles a self-contained Python CLI."
|
||||
category: research
|
||||
risk: safe
|
||||
source: community
|
||||
source_repo: xwmxcz/papers-skill
|
||||
source_type: community
|
||||
date_added: "2026-06-11"
|
||||
author: xwmxcz
|
||||
tags: [research, academic, papers, citations, arxiv, semantic-scholar, pdf]
|
||||
tools: [claude-code, antigravity, cursor, gemini-cli, codex-cli, opencode]
|
||||
license: "MIT"
|
||||
license_source: "https://github.com/xwmxcz/papers-skill/blob/main/LICENSE"
|
||||
---
|
||||
|
||||
# Papers Skill
|
||||
|
||||
## Overview
|
||||
|
||||
Papers Skill turns a coding agent into a literature-research assistant. It
|
||||
orchestrates a bundled Python CLI (`scripts/papers.py`) that hits the free
|
||||
Semantic Scholar and arXiv APIs, downloads arXiv PDFs, and extracts text with
|
||||
PyMuPDF. The agent decides which subcommand to invoke and how to combine
|
||||
results into a literature scan, a deep read of one paper, an impact analysis,
|
||||
or a reading list.
|
||||
|
||||
This skill is the Skill-mode port of the
|
||||
[papers-mcp](https://github.com/xwmxcz/papers-mcp) MCP server by the same
|
||||
author. Both projects share the same feature set; this one ships as a
|
||||
Claude Code plugin so it can be installed with a single command and needs no
|
||||
long-running MCP process.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when the user asks to search academic papers by topic, author, or venue.
|
||||
- Use when the user names a specific paper (by DOI, arXiv ID, or title) and
|
||||
wants metadata, the abstract, the TL;DR, or its reference list.
|
||||
- Use when the user wants to find work that **cites** a known paper (impact
|
||||
analysis, follow-up tracking).
|
||||
- Use when the user wants to download an arXiv PDF and have it summarized.
|
||||
- Use when the user asks to build a reading list around a topic.
|
||||
|
||||
## Do Not Use This Skill When
|
||||
|
||||
- The user wants paywalled non-arXiv full text. This skill cannot bypass
|
||||
publisher paywalls; it can only fetch arXiv PDFs and metadata everywhere.
|
||||
- The user wants OCR over scanned PDFs. PyMuPDF extracts embedded text only;
|
||||
scanned image-PDFs return the fallback message and need a separate OCR step.
|
||||
- The user wants real-time citation alerts or RSS-style watching. This skill
|
||||
is request-driven.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Verify dependencies
|
||||
|
||||
Three Python packages are required. The skill should check once per session,
|
||||
using the **same interpreter** to import-check and install so the dependency
|
||||
check and install target stay in sync:
|
||||
|
||||
```bash
|
||||
python -c "import httpx, arxiv, fitz" 2>&1 || python -m pip install httpx arxiv PyMuPDF
|
||||
```
|
||||
|
||||
If `python` is not on PATH, fall back to `py` (Windows launcher) or the
|
||||
absolute interpreter path — and remember to invoke pip via the same
|
||||
interpreter, e.g. `py -m pip install httpx arxiv PyMuPDF`.
|
||||
|
||||
### Step 2: Invoke the bundled CLI
|
||||
|
||||
The script lives at `${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py`
|
||||
and is bundled with this skill (no separate install needed). Always quote the
|
||||
path so it survives spaces.
|
||||
|
||||
```bash
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" <subcommand> [args]
|
||||
```
|
||||
|
||||
### Step 3: Pick the right subcommand
|
||||
|
||||
| Subcommand | Purpose | Example |
|
||||
|---|---|---|
|
||||
| `search <query> [--limit N]` | Semantic Scholar search, max 20 | `search "diffusion models" --limit 5` |
|
||||
| `detail <paper_id>` | Full metadata, TL;DR, top references | `detail 10.48550/arXiv.2310.06825` |
|
||||
| `citations <paper_id> [--limit N]` | Papers citing this one, max 20 | `citations <id> --limit 15` |
|
||||
| `arxiv <query> [--max-results N]` | arXiv preprint search, max 10 | `arxiv "RLHF" --max-results 5` |
|
||||
| `download <arxiv_id> [--save-dir D]` | Save PDF locally | `download 2310.06825 --save-dir ./pdfs` |
|
||||
| `read <pdf_path> [--max-pages N]` | Extract PDF text via PyMuPDF | `read ./pdfs/foo.pdf --max-pages 20` |
|
||||
|
||||
`detail` and `citations` auto-detect the ID type: DOIs starting with `10.`
|
||||
are used as-is, bare numeric IDs of 10+ digits are treated as arXiv IDs, and
|
||||
long hex strings are treated as Semantic Scholar `paperId`s.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Literature scan on a topic
|
||||
|
||||
```bash
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" search "retrieval augmented generation" --limit 10
|
||||
```
|
||||
|
||||
Present results as a ranked table with **# | Title | Year | Citations | ID**,
|
||||
then ask the user which papers to dig into.
|
||||
|
||||
### Example 2: Deep-read one paper
|
||||
|
||||
```bash
|
||||
# 1. Confirm match
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" detail 2005.11401
|
||||
# 2. Download
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" download 2005.11401 --save-dir ./pdfs
|
||||
# 3. Extract abstract + intro + conclusion
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" read ./pdfs/2005.11401v4.RAG.pdf --max-pages 10
|
||||
```
|
||||
|
||||
Summarize as: **problem · method · key result · limitations**.
|
||||
|
||||
### Example 3: Impact analysis on an anchor paper
|
||||
|
||||
```bash
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" detail 10.48550/arXiv.2005.11401
|
||||
python "${CLAUDE_PLUGIN_ROOT}/skills/papers-skill/scripts/papers.py" citations 10.48550/arXiv.2005.11401 --limit 20
|
||||
```
|
||||
|
||||
Cluster the citing papers by year/theme and highlight the most-cited
|
||||
follow-ups.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- ✅ Always call `detail` before `download` to confirm the paper matches user
|
||||
intent. Skipping this leads to wrong PDFs being fetched.
|
||||
- ✅ Include the paper ID alongside every title in your output so the user
|
||||
can re-query precisely.
|
||||
- ✅ Cite as `[FirstAuthor et al., Year] *Title* (cites: N)`.
|
||||
- ✅ For PDFs you download, always report the absolute save path.
|
||||
- ❌ Don't crawl. The script auto-retries 429s with exponential backoff;
|
||||
don't pile on parallel queries.
|
||||
- ❌ Don't raise `--max-pages` to 100+ without warning the user — it can
|
||||
consume a large amount of context.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The skill cannot fetch full text from paywalled publishers (Elsevier,
|
||||
Springer, Wiley, etc.). It can only read open arXiv PDFs.
|
||||
- PyMuPDF extracts embedded text only. Scanned image-PDFs return the
|
||||
fallback message `PDF无法提取文本(可能是扫描件)`; offer the user an
|
||||
alternative version or note that OCR is required.
|
||||
- Semantic Scholar's anonymous tier rate-limits aggressively. The script
|
||||
retries 3× with exponential backoff; persistent 429s during heavy use
|
||||
surface as `搜索失败: rate limit, retries exhausted`.
|
||||
- This skill does not replace environment-specific validation, testing, or
|
||||
expert review. Stop and ask for clarification if required inputs are
|
||||
missing.
|
||||
|
||||
## Security & Safety Notes
|
||||
|
||||
- The CLI performs **outbound HTTPS only** to `api.semanticscholar.org` and
|
||||
`arxiv.org` (and the arXiv-listed mirror for the bundled `arxiv` package).
|
||||
No authentication tokens are sent.
|
||||
- `download` writes a PDF to the directory the user specifies (default: the
|
||||
current working directory). Confirm the save path with the user before
|
||||
downloading to an unexpected location.
|
||||
- `read` opens a local PDF file with PyMuPDF — make sure the path the user
|
||||
supplies is one they trust.
|
||||
- No credentials or API keys are needed or stored anywhere.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Problem:** `需要安装 arxiv: pip install arxiv` or `需要安装 PyMuPDF: pip install PyMuPDF`.
|
||||
**Solution:** The script returns this friendly message instead of crashing
|
||||
when an optional dependency is missing. Offer to run the install command.
|
||||
|
||||
- **Problem:** `搜索失败: rate limit, retries exhausted` from `search` or
|
||||
`detail` or `citations`.
|
||||
**Solution:** Semantic Scholar is rate-limiting. Wait ~10 seconds and
|
||||
retry once. For repeated runs, fall back to `arxiv` for arXiv-indexed work.
|
||||
|
||||
- **Problem:** `download` fails with `找不到 arXiv ID: …`.
|
||||
**Solution:** The user gave a non-arXiv ID (likely a DOI for a non-arXiv
|
||||
paper). Use `detail` to inspect; only papers with an `externalIds.ArXiv`
|
||||
field can be downloaded.
|
||||
|
||||
- **Problem:** Garbled Chinese output on Windows.
|
||||
**Solution:** The script already forces UTF-8 stdout. If the host
|
||||
terminal is still misconfigured, set `PYTHONIOENCODING=utf-8` in the
|
||||
shell environment.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Skill home (this plugin): https://github.com/xwmxcz/papers-skill
|
||||
- Upstream MCP server: https://github.com/xwmxcz/papers-mcp
|
||||
- Semantic Scholar API docs: https://api.semanticscholar.org/
|
||||
- arXiv API docs: https://info.arxiv.org/help/api/
|
||||
- PyMuPDF docs: https://pymupdf.readthedocs.io/
|
||||
@@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
papers.py — Standalone academic paper toolkit (Skill-mode port of papers-mcp).
|
||||
Original MCP project: https://github.com/xwmxcz/papers-mcp
|
||||
|
||||
Usage:
|
||||
python papers.py search <query> [--limit 10]
|
||||
python papers.py detail <paper_id>
|
||||
python papers.py citations <paper_id> [--limit 10]
|
||||
python papers.py arxiv <query> [--max-results 5]
|
||||
python papers.py download <arxiv_id> [--save-dir .]
|
||||
python papers.py read <pdf_path> [--max-pages 10]
|
||||
|
||||
Dependencies: httpx, arxiv, PyMuPDF
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# Force UTF-8 stdout on Windows so Chinese strings render correctly when
|
||||
# called via Bash / cmd / cron (Python 3.7+).
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
import httpx
|
||||
|
||||
S2_BASE = "https://api.semanticscholar.org/graph/v1"
|
||||
S2_FIELDS = "paperId,title,abstract,year,citationCount,authors,externalIds,url"
|
||||
S2_RETRIES = 3
|
||||
S2_WAIT = 2 # seconds, exponential backoff base
|
||||
|
||||
|
||||
# ---------- HTTP helpers ----------
|
||||
|
||||
def _s2_get(url: str, params: dict) -> dict:
|
||||
"""GET with rate-limit retry. Returns parsed JSON or {'error': ...}."""
|
||||
for attempt in range(S2_RETRIES):
|
||||
try:
|
||||
r = httpx.get(
|
||||
url,
|
||||
params=params,
|
||||
timeout=30.0,
|
||||
headers={"User-Agent": "papers-skill/1.0"},
|
||||
)
|
||||
if r.status_code == 429:
|
||||
time.sleep(S2_WAIT * (attempt + 1))
|
||||
continue
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
except httpx.HTTPError as e:
|
||||
if attempt == S2_RETRIES - 1:
|
||||
return {"error": f"HTTP error: {e}"}
|
||||
time.sleep(S2_WAIT * (attempt + 1))
|
||||
return {"error": "rate limit, retries exhausted"}
|
||||
|
||||
|
||||
def _fmt_authors(authors: list, n: int = 3) -> str:
|
||||
if not authors:
|
||||
return "(unknown)"
|
||||
names = [a.get("name", "?") for a in authors[:n]]
|
||||
suffix = " et al." if len(authors) > n else ""
|
||||
return ", ".join(names) + suffix
|
||||
|
||||
|
||||
# ---------- Commands ----------
|
||||
|
||||
def cmd_search(args) -> str:
|
||||
data = _s2_get(
|
||||
f"{S2_BASE}/paper/search",
|
||||
{"query": args.query, "limit": min(args.limit, 20), "fields": S2_FIELDS},
|
||||
)
|
||||
if "error" in data:
|
||||
return f"搜索失败: {data['error']}"
|
||||
papers = data.get("data", [])
|
||||
if not papers:
|
||||
return f"没有找到与 '{args.query}' 相关的论文"
|
||||
out = [f"# 搜索结果 ({len(papers)} 篇)\n"]
|
||||
for i, p in enumerate(papers, 1):
|
||||
title = p.get("title", "无标题")
|
||||
year = p.get("year", "?")
|
||||
citations = p.get("citationCount", 0)
|
||||
authors = _fmt_authors(p.get("authors", []))
|
||||
abstract = (p.get("abstract") or "").strip()[:200]
|
||||
ext = p.get("externalIds") or {}
|
||||
arxiv_id = ext.get("ArXiv", "")
|
||||
out.append(
|
||||
f"## {i}. {title}\n"
|
||||
f"**Authors:** {authors} \n"
|
||||
f"**Year:** {year} | **Citations:** {citations} \n"
|
||||
f"**S2 ID:** `{p.get('paperId')}`"
|
||||
+ (f" | **arXiv:** `{arxiv_id}`" if arxiv_id else "")
|
||||
+ " \n"
|
||||
f"**Abstract:** {abstract}{'...' if abstract else '(无摘要)'}\n"
|
||||
)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def cmd_detail(args) -> str:
|
||||
pid = args.paper_id
|
||||
# Auto-detect ID type
|
||||
if pid.startswith(("10.", "ARXIV:", "DOI:", "MAG:", "PMID:", "PMCID:")):
|
||||
lookup = pid
|
||||
elif pid.isdigit() and len(pid) >= 10:
|
||||
lookup = f"ARXIV:{pid}"
|
||||
else:
|
||||
lookup = pid # assume raw S2 paperId
|
||||
fields = S2_FIELDS + ",references.title,references.year,tldr"
|
||||
data = _s2_get(f"{S2_BASE}/paper/{lookup}", {"fields": fields})
|
||||
if "error" in data:
|
||||
return f"查询失败: {data['error']}"
|
||||
title = data.get("title", "无标题")
|
||||
authors = _fmt_authors(data.get("authors", []), n=5)
|
||||
year = data.get("year", "?")
|
||||
citations = data.get("citationCount", 0)
|
||||
abstract = data.get("abstract") or "(无摘要)"
|
||||
tldr = (data.get("tldr") or {}).get("text") or "(无 TL;DR)"
|
||||
refs = (data.get("references") or [])[:10]
|
||||
out = [
|
||||
f"# {title}",
|
||||
f"**Authors:** {authors} ",
|
||||
f"**Year:** {year} | **Citations:** {citations} ",
|
||||
f"**ID:** `{data.get('paperId')}` ",
|
||||
f"**URL:** {data.get('url', '')}",
|
||||
"",
|
||||
"## TL;DR",
|
||||
tldr,
|
||||
"",
|
||||
"## Abstract",
|
||||
abstract,
|
||||
"",
|
||||
f"## Top {len(refs)} References",
|
||||
]
|
||||
for i, r in enumerate(refs, 1):
|
||||
out.append(f"{i}. {r.get('title', '?')} ({r.get('year', '?')})")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def cmd_citations(args) -> str:
|
||||
data = _s2_get(
|
||||
f"{S2_BASE}/paper/{args.paper_id}/citations",
|
||||
{
|
||||
"limit": min(args.limit, 20),
|
||||
"fields": "title,year,authors",
|
||||
},
|
||||
)
|
||||
if "error" in data:
|
||||
return f"查询失败: {data['error']}"
|
||||
cites = data.get("data", [])
|
||||
if not cites:
|
||||
return "没有找到引用此论文的记录"
|
||||
out = [f"# 引用此论文的论文 ({len(cites)} 篇)\n"]
|
||||
for i, item in enumerate(cites, 1):
|
||||
p = item.get("citingPaper", {})
|
||||
title = p.get("title", "?")
|
||||
year = p.get("year", "?")
|
||||
authors = _fmt_authors(p.get("authors", []), n=2)
|
||||
out.append(f"{i}. **{title}** ({year}) — {authors}")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def cmd_arxiv(args) -> str:
|
||||
try:
|
||||
import arxiv
|
||||
except ImportError:
|
||||
return "需要安装 arxiv: pip install arxiv"
|
||||
search = arxiv.Search(
|
||||
query=args.query,
|
||||
max_results=min(args.max_results, 10),
|
||||
sort_by=arxiv.SortCriterion.Relevance,
|
||||
)
|
||||
results = list(arxiv.Client().results(search))
|
||||
if not results:
|
||||
return f"没有找到与 '{args.query}' 相关的 arXiv 论文"
|
||||
out = [f"# arXiv 搜索结果 ({len(results)} 篇)\n"]
|
||||
for i, p in enumerate(results, 1):
|
||||
arxiv_id = p.entry_id.rsplit("/", 1)[-1]
|
||||
out.append(
|
||||
f"## {i}. {p.title}\n"
|
||||
f"**Authors:** {', '.join(a.name for a in p.authors[:3])} \n"
|
||||
f"**arXiv ID:** `{arxiv_id}` \n"
|
||||
f"**Published:** {p.published.strftime('%Y-%m-%d')} \n"
|
||||
f"**Summary:** {p.summary[:200].strip()}...\n"
|
||||
)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def cmd_download(args) -> str:
|
||||
try:
|
||||
import arxiv
|
||||
except ImportError:
|
||||
return "需要安装 arxiv: pip install arxiv"
|
||||
save_dir = Path(args.save_dir).resolve()
|
||||
save_dir.mkdir(parents=True, exist_ok=True)
|
||||
search = arxiv.Search(id_list=[args.arxiv_id])
|
||||
paper = next(arxiv.Client().results(search), None)
|
||||
if paper is None:
|
||||
return f"找不到 arXiv ID: {args.arxiv_id}"
|
||||
path = paper.download_pdf(dirpath=str(save_dir))
|
||||
return f"已下载: {path}"
|
||||
|
||||
|
||||
def cmd_read(args) -> str:
|
||||
try:
|
||||
import fitz # PyMuPDF
|
||||
except ImportError:
|
||||
return "需要安装 PyMuPDF: pip install PyMuPDF"
|
||||
pdf = Path(args.pdf_path)
|
||||
if not pdf.exists():
|
||||
return f"PDF 不存在: {pdf}"
|
||||
doc = fitz.open(str(pdf))
|
||||
pages = min(args.max_pages, doc.page_count)
|
||||
chunks = []
|
||||
for i in range(pages):
|
||||
text = doc.load_page(i).get_text().strip()
|
||||
if text:
|
||||
chunks.append(f"--- Page {i + 1} ---\n{text}")
|
||||
doc.close()
|
||||
if not chunks:
|
||||
return "PDF无法提取文本(可能是扫描件)"
|
||||
return "\n\n".join(chunks)
|
||||
|
||||
|
||||
# ---------- CLI ----------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="papers", description=__doc__)
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p = sub.add_parser("search", help="Semantic Scholar 搜索")
|
||||
p.add_argument("query")
|
||||
p.add_argument("--limit", type=int, default=10)
|
||||
p.set_defaults(fn=cmd_search)
|
||||
|
||||
p = sub.add_parser("detail", help="论文详情 (支持 DOI / ARXIV:id / S2 paperId)")
|
||||
p.add_argument("paper_id")
|
||||
p.set_defaults(fn=cmd_detail)
|
||||
|
||||
p = sub.add_parser("citations", help="该论文的引用列表")
|
||||
p.add_argument("paper_id")
|
||||
p.add_argument("--limit", type=int, default=10)
|
||||
p.set_defaults(fn=cmd_citations)
|
||||
|
||||
p = sub.add_parser("arxiv", help="arXiv 搜索")
|
||||
p.add_argument("query")
|
||||
p.add_argument("--max-results", type=int, default=5)
|
||||
p.set_defaults(fn=cmd_arxiv)
|
||||
|
||||
p = sub.add_parser("download", help="下载 arXiv PDF")
|
||||
p.add_argument("arxiv_id")
|
||||
p.add_argument("--save-dir", default=".")
|
||||
p.set_defaults(fn=cmd_download)
|
||||
|
||||
p = sub.add_parser("read", help="提取 PDF 文本 (PyMuPDF)")
|
||||
p.add_argument("pdf_path")
|
||||
p.add_argument("--max-pages", type=int, default=10)
|
||||
p.set_defaults(fn=cmd_read)
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
print(args.fn(args))
|
||||
except Exception as e:
|
||||
print(f"错误: {type(e).__name__}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user