Imported from mmgeorge/papers (
crates/papers-cli/AGENTS.md). Install upstream withnpx skills add mmgeorge/papers --skill papers-cli. Copyright stays with the author.
papers-cli
CLI binary over the papers shared crate. Provides a papers command for querying
the OpenAlex academic research database and your personal Zotero reference library.
Architecture
src/
main.rs — tokio main; parse Cli; dispatch to papers::api::* functions
cli.rs — all clap structs (Cli, EntityCommand, WorkCommand, ZoteroCommand, etc.)
format.rs — human-readable text formatters for each entity/response type
tests/
cli.rs — wiremock integration tests (format output + slim JSON assertions)
Imports only papers::* for OpenAlex — no direct dependency on papers-openalex.
Imports papers_zotero::ZoteroClient directly for Zotero commands.
CLI commands
OpenAlex commands
papers work list [-s <query>] [-f <filter>] [--sort <field>] [-n <per_page=10>]
[--page <n>] [--cursor <c>] [--sample <n>] [--seed <n>] [--json]
papers work get <id> [--json]
papers work autocomplete <query> [--json]
papers work find <query> [-n <count>] [-f <filter>] [--json]
papers author list / get / autocomplete
papers source list / get / autocomplete
papers institution list / get / autocomplete
papers topic list / get
papers publisher list / get / autocomplete
papers funder list / get / autocomplete
Zotero commands
Requires ZOTERO_USER_ID and ZOTERO_API_KEY env vars. Exits with error if not set.
papers zotero work list [-s <q>] [--tag <t>] [--type <t>] [--sort <f>] [-n <n>] [--json]
papers zotero work get <key> [--json]
papers zotero work collections <key> [--json]
papers zotero work notes <key> [-n <n>] [--json]
papers zotero work attachments <key> [-n <n>] [--json]
papers zotero work annotations <key> [--json]
papers zotero work tags <key> [-q <q>] [--json]
papers zotero attachment list [-s <q>] [--sort <f>] [-n <n>] [--json]
papers zotero attachment get <key> [--json]
papers zotero attachment file <key> -o <output-path>
papers zotero annotation list [-n <n>] [--json]
papers zotero annotation get <key> [--json]
papers zotero note list [-s <q>] [-n <n>] [--json]
papers zotero note get <key> [--json]
papers zotero collection list [--sort <f>] [-n <n>] [--top] [--json]
papers zotero collection get <key> [--json]
papers zotero collection works <key> [-s <q>] [--type <t>] [--sort <f>] [-n <n>] [--json]
papers zotero collection attachments <key> [-n <n>] [--json]
papers zotero collection notes <key> [-s <q>] [-n <n>] [--json]
papers zotero collection annotations <key> [--json]
papers zotero collection subcollections <key> [--sort <f>] [-n <n>] [--json]
papers zotero collection tags <key> [-q <q>] [--top] [--json]
papers zotero tag list [-q <q>] [--sort <f>] [-n <n>] [--top] [--trash] [--json]
papers zotero tag get <name> [--json]
papers zotero search list [--json]
papers zotero search get <key> [--json]
papers zotero group list [--json]
papers zotero extract list [-s <q>] [-n <n>] [--json]
papers zotero extract text <key|doi|title>
papers zotero extract json <key|doi|title>
papers zotero extract get <key|doi|title>
extract list shows all items that have a DataLab extraction in either the
local cache or Zotero (papers_extract_{key}.zip backup), with two checkmarks per item:
[✓ local] [✓ zotero]. Items with neither are omitted.
extract text / extract json / extract get are read-only — they never
invoke DataLab. Use papers db work add <key> to run extraction and index the paper.
Default output is human-readable text. Add --json for raw JSON.
Default --per-page is 10 (vs API default of 25).
Output modes
- Text (default): formatted for human reading — titles, authors, key stats
- JSON (
--json): pretty-printed JSON frompapers::api::*— list tools return slimSlimListResponse<XxxSummary>; get tools return full entity
work find auth guard
Before calling the API, main.rs checks std::env::var("OPENALEX_KEY").
If absent, exits with an error message and non-zero status.
Do not call the API if the key is missing.
How to add a new OpenAlex command
- Add the clap variant to the appropriate
*Commandenum incli.rs - Add a formatter in
format.rs(text output) - Wire the new variant into the
matchinmain.rs, callingpapers::api::* - Add tests in
tests/cli.rs— at least text + JSON modes
How to add a new Zotero command
- Add the variant to the appropriate
Zotero*Commandenum incli.rs - Add a formatter in
format.rsusingpapers_zoterotypes - Wire into the
EntityCommand::Zotero { cmd }arm inmain.rs, callingZoteroClientdirectly - Add tests in
tests/cli.rs— Zotero commands call client methods directly (notpapers::api::*)
Zotero client initialization
Two helpers in main.rs:
-
zotero_client()— required Zotero client; used by thepapers zotero *subcommands. ReturnsErron any failure (not configured, not running, etc.). The Zotero arm calls it and exits early on error. -
optional_zotero()— optional Zotero enrichment; used bywork getandwork textwhere Zotero provides extra context but is not strictly required.- Returns
Ok(Some(client))when Zotero is configured and reachable - Returns
Ok(None)when Zotero env vars are absent (silently omit Zotero info) - Returns
Err(NotRunning)when Zotero is installed on disk but not running → caller callsexit_err()to surface the actionable message - Set
ZOTERO_CHECK_LAUNCHED=0to makeoptional_zotero()returnOk(None)instead ofErrwhen Zotero is installed but not running
- Returns
Key notes
format.rsformatters take references to response types frompapers::*orpapers_zotero::*- Formatters for slim types (
SlimListResponse<XxxSummary>) for OpenAlex list commands - Formatters for full entity types (
Work,Author, etc.) for OpenAlex get commands format_autocompleteandformat_find_worksare shared across all entities- All list commands default to
per_page = 10 exit_errin main.rs prints to stderr and exits with code 1
Running the CLI for testing
Use cargo run --release which automatically sets env vars from .cargo/config.toml
(including ORT_DYLIB_PATH for ONNX runtime and PDFIUM_PATH for PDF extraction).
All required keys (ZOTERO_API_KEY, ZOTERO_USER_ID) must be in the shell environment.
# Preferred — cargo sets .cargo/config.toml env vars automatically
cargo run --release -- db work add U9PRIZJ7
cargo run --release -- db chunk search "some query"
# Direct binary — must set ORT_DYLIB_PATH manually (not read from .cargo/config.toml)
ORT_DYLIB_PATH=D:/code/papers/.cache/onnxruntime.dll target/release/papers.exe db chunk search "query"
# wrong — don't do this (no ORT runtime, embedding will fail)
target/release/papers.exe db chunk search "query"
Required env vars (.cargo/config.toml)
| Variable | Value | Purpose |
|---|---|---|
ORT_DYLIB_PATH |
.cache/onnxruntime.dll (relative) |
ONNX Runtime for embeddings |
PDFIUM_PATH |
.cache (relative) |
pdfium library for PDF extraction |
PROTOC |
path to protoc.exe |
Protocol Buffers compiler (build dep) |
Required env vars (shell)
| Variable | Purpose |
|---|---|
ZOTERO_USER_ID |
Zotero library access |
ZOTERO_API_KEY |
Zotero API authentication |
Integration test cache directory
Tests that write fake DataLab cache files must NOT write to the production
papers/datalab directory. Instead, redirect the cache by setting the
PAPERS_DATALAB_CACHE_DIR environment variable before any test code runs:
static INIT: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
fn test_cache_base() -> &'static std::path::PathBuf {
INIT.get_or_init(|| {
let dir = dirs::cache_dir().unwrap().join("papers").join("test");
std::fs::create_dir_all(&dir).unwrap();
unsafe { std::env::set_var("PAPERS_DATALAB_CACHE_DIR", &dir) };
dir
})
}
- Write to:
{cache_dir}/papers/test/{key}/(NOTpapers/datalab) PAPERS_DATALAB_CACHE_DIRis read bydatalab_cache_dir()anddatalab_cached_item_keys()inpapers-core- Use per-test unique key namespaces (e.g.,
EXT00101) to prevent parallel test interference - Use a drop-guard (
CacheCleanup) to remove test dirs even on panic - See
tests/extract.rsfor the canonical pattern
Key gotchas
- Do NOT add
papers-openalexas a direct dependency — usepapers::*only FindWorksParamsbuilder uses consumed-builder pattern for optional fields: use intermediatelet mut builder = ...+if let Some(v) = opt { builder = builder.field(v); }ZoteroClientparams (ItemListParams,CollectionListParams,TagListParams) use struct literal construction:ItemListParams { item_type: Some("note".into()), limit, start, ..Default::default() }Do NOT use the builder — bon's type-state changes the generic on each.field()call, making mutable variable reassignment impossible.- Zotero commands call
ZoteroClient::from_env()at the top of theZoteroarm; exit early if Err