ref-manager

Commands · Keep the library healthy

Maintain

Re-check papers for retractions, rebuild the search index from the files on disk, see what's configured, and move papers into Papers.app and your annotations back out. Files are the authority; these commands refresh projections and flag what changed without rewriting history.

How they fit

/ref:audit retraction re-check --citations observations propagate → stale artifacts /ref:index --rebuild recreate catalog refresh relation staleness /ref:status read-only counts <library_root>/ papers/<pmid>/meta.json retraction_status (never downgraded on failure) papers/<pmid>/citations.json dated observations, append-only papers/<pmid>/claim_registry.json claim status: active · superseded graph/relations.jsonl stale: true when evidence is no longer active index/catalog.sqlite FTS5 projection, deleted and recreated papers/<pmid>/raw/<sha256>/source.pdf acquired PDF (immutable) papers/<pmid>/annotations.json Papers highlights, tombstoned not deleted ~/.config/ref-manager/config.json Papers.app outside ref-manager; its *.db is only read via a snapshot OUTBOUND /ref:open /ref:pull-annotations INBOUND
writes library records reads only Papers.app handoff
The catalog is a projection: /ref:index --rebuild deletes and recreates it from committed files, so it is always safe to run. /ref:open hands the OS a file with open -a Papers; /ref:pull-annotations reads a SQLite backup copy of the Papers database. Neither writes to Papers' database.

Commands

/ref:audit

phase 11PubMed MCP

Re-check retraction and erratum status across the library, or record and show dated cited-by observations.

/ref:audit [selector] /ref:audit --citations [selector] /ref:audit show-citations [--stale-days N] [selector]

Arguments

[selector]
Optional selector (<pmid…>, --project, --study, --search, --from-file, …) to scope the sweep. Unlike other selector commands, no selector means the whole library, not an error: this is a maintenance sweep, not a saved set.
--citations
Record citation observations instead of retraction checks. Run the two modes separately.
show-citations
An action, not a flag. Print each PMID's latest real observation and whether it is stale, without checking anything new.
--stale-days N
For show-citations: age threshold in days (default 180).

What happens

Default mode (retraction status):

  1. For each resolved PMID, Claude calls get_article_metadata and checks article_types for a retraction, erratum or correction marker. This is the same check /ref:extract runs at ingestion.
  2. A successful check updates retraction_status in meta.json and reports whether it changed. A failed check leaves the prior status untouched and prints a diagnostic. It never guesses "none" or downgrades an earlier "retracted".
  3. If any status changed, propagate lists saved /ref:brief, /ref:compare and /ref:summarize artifacts that used those papers. They are not refreshed automatically; you run --refresh on them, and their earlier frozen versions are kept.

--citations mode: for each PMID, a paper with no PMCID gets a no_pmcid marker, a failed lookup gets a check_failed marker, and a real count is stored with its source, query, coverage note and retrieval date. Every call appends to citations.json; earlier entries are never overwritten, and a failure never becomes a zero.

No citation-count source is wired in yet. None of the PubMed MCP tools available return a PMC cited-by count, and related-article similarity is not citation. The storage and display machinery is built and tested, but until a real source exists, --citations has nothing to record and the command says so instead of inventing counts.

show-citations: skips check_failed and no_pmcid markers and shows the most recent real observation. A PMID with none shows null, which means "no observation available", not zero.

Reads & writes

Retraction mode updates papers/<pmid>/meta.json (retraction_status). --citations appends to papers/<pmid>/citations.json. propagate and show-citations only read: saved artifact manifests under tables/, summaries/, briefs/ (library-wide and per project), and citations.json.

Example

output from a scratch library (check results written by hand)

 /ref:audit
31452104: checked (none -> retracted) [CHANGED]
30679568: check_failed -- prior status retained (unknown); get_article_metadata timed out
# followed by the same results as JSON, ending in:
  "changed_pmids": [
    "31452104"
  ]
# propagate: which saved artifacts used the changed paper
{
  "changed_pmids": [
    "31452104"
  ],
  "potentially_stale_artifacts": []
}
 /ref:audit show-citations 31452104
[
  {
    "pmid": "31452104",
    "latest_observation": null,
    "stale": null
  }
]

Under the hood

audit.py retraction --repo <library_root> --results-file <json> [selector], then audit.py propagate --repo <library_root> --results-file <changed-pmids-json>; audit.py citations --repo … --results-file … [selector]; audit.py show-citations --repo … [--stale-days N] [selector]. The script never calls PubMed; Claude supplies the check results as JSON.

See also

/ref:report (--citations) · /ref:brief · /ref:compare · /ref:summarize · /ref:index

/ref:index

phase 0refresh: phase 8

Rebuild the SQLite search catalog from committed records and flag graph relations whose evidence has changed.

/ref:index --rebuild

Arguments

--rebuild
Required. It is currently the only supported mode.

What happens

  1. Deletes index/catalog.sqlite and recreates it from scratch. Files are the authority and the catalog is only a projection, so rebuilding beats migrating in place.
  2. Indexes one row per paper from meta.json (including retraction status), paragraphs of each paper's current source.md into passages_fts (headings become section labels), and every claim in claim_registry.json, whether active or superseded, into claims and claims_fts.
  3. Only what current.json points at is read; incomplete staging directories are ignored. Running it twice gives the same result.
  4. If graph/relations.jsonl exists, a relation refresh sets stale: true on any relation whose supporting claims are no longer active. A review decision is never cleared, only flagged for re-confirmation, and the stale relation IDs are reported.

Reads & writes

Reads papers/*/meta.json, current.json, versions/<v>/source.md, claim_registry.json. Writes index/catalog.sqlite and, when present, updates graph/relations.jsonl (stale, stale_reason).

Example

output from a scratch library

 /ref:index --rebuild
catalog rebuilt: 2 paper(s), 0 passage(s), 1 claim(s) indexed
# passages come only from source.md in the version current.json names;
# here /ref:extract committed last, and its version holds claims, not source.md
# with a graph present, the relation refresh then prints JSON of the form
# {"relations_checked": N, "went_stale": [relation IDs]}

Under the hood

catalog.py rebuild --repo <library_root>, then, if graph/relations.jsonl exists, relation.py refresh --repo <library_root>.

See also

/ref:status · /ref:search · /ref:weave · Concepts

/ref:status

phase 0read-only

Show which library is configured, catalog counts, extraction-tier breakdown, source completeness, project summaries, and a few recent papers. Read-only.

/ref:status

Arguments

None.

What happens

  1. Reads the config file. If none exists, it errors and points you at /ref:init; it never guesses a path.
  2. Prints the library root, and errors if that directory no longer exists.
  3. Prints the number of papers in the catalog (or a hint to run /ref:index --rebuild if the catalog hasn't been built), an extraction-tier breakdown, and source/completeness counts.
  4. Prints per-project summaries and, where useful, a short list of recent papers.
  5. Each recent paper carries a concise source badge — full-text, pdf-backed, oa-pending, abstract-only, or metadata-only — so you can see acquisition state at a glance without opening records. Claude keeps this compact; the goal is quick orientation, not a substitute for full search.

Reads & writes

Reads ~/.config/ref-manager/config.json, index/catalog.sqlite, projects/, and papers' meta.json for source/completeness state. Writes nothing.

Example

output from a scratch library (paths shortened)

 /ref:status
library: /private/tmp/…/scratch/lib
catalog: not built yet (run /ref:index --rebuild)
projects: 0
 /ref:index --rebuild
 /ref:status
library: /private/tmp/…/scratch/lib
papers indexed: 2
by extraction tier: {"abstract": 1, "full": 1, "unavailable": 0}
by source completeness: {"full-text": 1, "abstract-only": 1}
projects: 0
recent papers:
  10000001 rivera2019cortical (full-text)
  10000002 chen2019editorial (abstract-only)

Under the hood

status.py (no arguments).

See also

/ref:init · /ref:index

/ref:open

phase 3Papers.app

Open a paper's acquired PDF in Papers.app for reading and annotating.

/ref:open <pmid>

Arguments

<pmid>
Required. The paper must be in the library.

What happens

  1. Finds the paper's acquired PDF under raw/<sha256>/.
  2. If there is none, reports no_pdf_acquired and points at /ref:fetch or /ref:attach. This is not an error.
  3. Otherwise runs open -a Papers <pdf>, just like double-clicking the file in Finder, and reports opened with the path.

This is the only ref-manager command that touches Papers.app itself, and it never touches the Papers database. Highlights you make there come back with /ref:pull-annotations.

Reads & writes

Reads papers/<pmid>/meta.json and the acquired PDF. Writes nothing in the library; Papers.app receives the file.

Example

output from a scratch library

 /ref:open 31452104
{
  "pmid": "31452104",
  "status": "no_pdf_acquired",
  "message": "no acquired PDF for this paper yet — run /ref:fetch or /ref:attach first"
}

illustrative output

 /ref:open 31452104
{
  "pmid": "31452104",
  "status": "opened",
  "path": "<library_root>/papers/31452104/raw/<sha256>/source.pdf"
}

Under the hood

open_in_papers.py <pmid> --repo <library_root>; PDF lookup shared with export_papers.py.

See also

/ref:pull-annotations · /ref:export-papers · Papers handoff tutorial

/ref:pull-annotations

phase 4Papers.app

Copy your Papers.app highlights and margin notes for one paper into its record, one-way and without loss.

/ref:pull-annotations <pmid>

Arguments

<pmid>
Required. The paper must already exist (/ref:add first).

What happens

  1. Claude looks for ~/Library/Application Support/Papers/*.db and passes the first match. If there is none, the result is no_papers_db_configured, not a failure.
  2. The live database is copied with SQLite's backup API from a read-only connection, and all reads happen on that copy. If no consistent copy can be taken, the result is snapshot_unavailable.
  3. The Papers item is matched by PMID, falling back to DOI. No match gives no_matching_papers_item, meaning the paper isn't in your Papers library.
  4. Annotations are upserted by their own Papers id, into added, updated or unchanged. A repeat pull with no upstream changes reports everything unchanged and never duplicates.
  5. An annotation deleted in Papers since the last pull is tombstoned (deleted_upstream: true with a timestamp), not deleted locally, and listed under tombstoned. An accidental deletion or sync hiccup in Papers never silently erases evidence you already had.

Your free-text notes.md (from /ref:note) is never touched; annotations and notes are stored separately.

Reads & writes

Reads papers/<pmid>/meta.json and a snapshot of the Papers database. Writes papers/<pmid>/annotations.json (with papers_item_id and last_pulled_at). Never writes to Papers.

Example

output from a scratch library

 /ref:pull-annotations 31452104
# no Papers database on this machine
{
  "pmid": "31452104",
  "status": "no_papers_db_configured"
}

illustrative output

 /ref:pull-annotations 31452104
{
  "pmid": "31452104",
  "status": "ok",
  "added": ["<annotation id>"],
  "updated": [],
  "tombstoned": ["<annotation id>"],
  "unchanged": ["<annotation id>", "<annotation id>"]
}

Under the hood

pull_annotations.py --repo <library_root> --pmid <pmid> [--papers-db <path>]; snapshot access via papers_snapshot.py.

See also

/ref:open · /ref:note · /ref:export-papers · Papers handoff tutorial