Operator utilities that wrap the precis-mcp package without going
through the precis CLI. Each command is a thin Bash wrapper that
picks an interpreter in this order:
- Shared workspace venv at
pips/.venv— the default since the single-venv migration. The workspace root'sdevdependency group pullsprecis-mcp[paper](acatome-extract + sentence-transformers) into this venv, so no per-package venv is required. - Legacy per-package venv at
packages/precis-mcp/.venv— kept as a fallback for checkouts that pre-date the migration. uv run --project <workspace>— last resort, on-demand sync.
The wrapper resolves these paths itself so scripts work regardless of the caller's CWD or shell.
Every wrapper sets sensible defaults if not already exported:
PRECIS_DATABASE_URL— defaults topostgresql://acatome:acatome@127.0.0.1:5432/precis(the canonical local v2 database).PRECIS_EMBEDDER— defaults tobge-m3(loadsBAAI/bge-m3viasentence-transformers).
Override either by exporting the variable before invoking the script.
Print counts of paper refs and (optionally) breakdowns.
./scripts/paper-count # papers + total blocks
./scripts/paper-count --by-kind # all kinds, sorted by count
./scripts/paper-count --by-provider # paper rows per provider
./scripts/paper-count --recent # 10 most recent papers
./scripts/paper-count --recent 50 # custom NWatch a directory for new top-level *.pdf files. For each one:
- Run
acatome-extract→ produces a.acatomebundle. - Insert the bundle via
Store.ingest_bundle(...)(idempotent on DOI / pdf_hash / arxiv_id). - On success: move the PDF + bundle into
<dir>/completed/. - On failure: move the PDF into
<dir>/errors/alongside a<stem>.error.logtraceback.
# default watch dir = /Users/bots/Documents/cluster/paper-ingest
./scripts/paper-monitor-ingest-dir
./scripts/paper-monitor-ingest-dir --once # one sweep, no loop
./scripts/paper-monitor-ingest-dir --interval 30 # poll every 30 s
./scripts/paper-monitor-ingest-dir --tag review-queue --tag urgent
./scripts/paper-monitor-ingest-dir --no-verify # skip metadata cross-check
./scripts/paper-monitor-ingest-dir --dir /some/other/inboxCtrl+C (or SIGTERM) drains the current PDF and exits cleanly.
For every paper in precis with a DOI or arXiv id, fetch the citation list from Semantic Scholar, filter by date window and relevance, dedup against the corpus, and write a markdown report plus a JSONL feed of unique citing papers.
./scripts/find-citing-papers # last 180 days, default relevance gate
./scripts/find-citing-papers --since 2026-02-01 # explicit window start
./scripts/find-citing-papers --until 2026-07-31 # explicit window end
./scripts/find-citing-papers --influential-only # require S2 isInfluential=True
./scripts/find-citing-papers --keep-background # keep background-only intents
./scripts/find-citing-papers --limit 100 # only the N most recently ingested source papers
./scripts/find-citing-papers --slug-prefix abazari # filter source corpus by slug
./scripts/find-citing-papers --no-fetch # aggregate from existing cache only
./scripts/find-citing-papers --force # ignore cache, refetch every paperNoise reduction (the full corpus returns ~900k unique citing papers — these flags are how you make a digestible report):
# Co-citation density: drop papers that cite fewer than N of ours.
# Strongest, cheapest signal. 909k → 212k @ 2; → 25k @ 5; → ~3k @ 10.
./scripts/find-citing-papers --no-fetch --min-co-cites 5
# Drop fresh preprints that haven't been cited yet (signal: traction).
./scripts/find-citing-papers --no-fetch --min-citing-citations 5
# Hard cap on output: top N after sort.
./scripts/find-citing-papers --no-fetch --top-n 200
# bge-m3 cosine rerank (the gold standard relevance gate). Loads
# the embedder once, embeds source corpus + surviving citing papers'
# title+abstract, drops anything below cosine similarity threshold.
# Adds ~80-100s per ~2.5k surviving citing papers on Apple Silicon.
./scripts/find-citing-papers --no-fetch --min-co-cites 5 --min-similarity 0.55
# Per-source-top-K: emit top K most-recent citations PER OUR paper
# (separate output mode, useful for "what's new for paper X" digests).
./scripts/find-citing-papers --no-fetch --per-source-top 5
# Recommended starting digest combo:
./scripts/find-citing-papers --no-fetch \
--since 2026-01-01 \
--min-co-cites 3 \
--min-citing-citations 1 \
--min-similarity 0.55 \
--top-n 200Sort precedence in global mode: co-citations DESC, similarity DESC (when computed), publication date DESC, title.
Per-paper results are cached as JSON under
paper-ingest/.citing-papers-cache/<slug>.json (override with
PRECIS_CITING_CACHE_DIR or --cache-dir) so the sweep is
resumable — re-runs reuse cache files unless --force is passed.
Reads SEMANTIC_SCHOLAR_API_KEY from the environment to raise S2's
free-tier rate limit. A full sweep on ~4k papers takes 1–3 hours
depending on rate-limit hits and how heavily-cited each source is.
The default markdown report goes to
paper-ingest/citing-papers-<UTC-timestamp>.md; the JSONL feed
sits next to it with .jsonl extension and is shaped for downstream
ingest (e.g. piping through acatome-quest submit).
One-shot sweep that walks every live paper ref and fully populates
ref_identifiers from Semantic Scholar's externalIds cluster
(DOI / ArXiv / PubMed / PubMedCentralID / MAG / DBLP / CorpusId /
OpenAlex). Migration 0009_ref_identifiers.sql backfilled the four
canonical schemes (DOI, arXiv id, S2 paperId, pdf_hash) from
existing meta JSON; this sweep adds everything else by re-asking S2.
After the sweep, doilist scan sees the maximum-coverage alias
index — sources/ DOIs that match ANY known identifier of any
ingested paper get caught, not just the canonical four.
./scripts/enrich-paper-identifiers --dry-run --limit 5 # sanity check
./scripts/enrich-paper-identifiers # full sweep
./scripts/enrich-paper-identifiers --limit 500 # first 500 only
./scripts/enrich-paper-identifiers --re-enrich # ignore the
# `s2-enriched`
# tag and re-queryIdempotent via the s2-enriched open tag attached to each ref on
completion. Re-runs of the script skip already-tagged refs unless
--re-enrich is passed. Failures are logged but don't abort the
sweep — re-run later to pick them up.
Reads SEMANTIC_SCHOLAR_API_KEY from the environment to raise S2's
free-tier rate limit. Without it the sweep still runs but is slower
and more likely to hit 429 errors. Fixed soft delay of 0.4s between
calls keeps us well under any S2 rate cap.
Estimated wall-clock: ~2.5s per paper, so ~100–175 min for a 4k-paper corpus (S2 latency dominates the inter-call sleep).
DOI triage / fetching / rewriting. Subcommands:
doilist scan— extract DOIs fromsources/, dedupe against precis (across every alias scheme), validate via doi.org, writedois_to_get.mdplus.doi_status.json.--downloadto also fetch the resulting OA PDFs via Unpaywall.doilist download— fetch PDFs for the queue file, slowly.doilist recheck— re-clean and re-validate previously-invalid DOIs.doilist skip <doi> --reason <tag>/doilist unskip <doi>— mark DOIs as retracted / paywalled / out-of-scope without dropping them from the cache.doilist convert-doi-to-slugs <dir>— see below.
Walk a directory and rewrite known DOIs to [slug] in text files,
in place. Best-effort: any DOI not in precis is left untouched.
Useful for the perplexity-summary triage workflow: drop a directory of summaries into a paper outline, then collapse every DOI string into the precis paper slug so downstream tools (LaTeX bib generation, agent prompts, search) can work with native identifiers instead of opaque DOI URLs.
# default: cwd, recurse, rewrite *.md and *.txt in place
./scripts/doilist convert-doi-to-slugs
# explicit directory + dry-run (counts only, no writes)
./scripts/doilist convert-doi-to-slugs --dry-run path/to/dir
# also rewrite .tex sources
./scripts/doilist convert-doi-to-slugs --ext md --ext txt --ext tex .Both URL-form (https://doi.org/10.x/y) and bare (10.x/y) DOIs are
matched; URL prefixes are consumed alongside the DOI so no broken
links remain. arXiv DOI form (10.48550/arXiv.<id>) maps to the
same slug as the bare arXiv id.
One bulk SQL JOIN over ref_identifiers × refs (kind='paper') builds
the slug map at startup and is reused across all files — typical
runtime is well under a second for a directory of markdown notes.
scripts/
_common.py # shared store/embedder helpers
_paper_count.py # python impl
_paper_monitor_ingest_dir.py # python impl
_find_citing_papers.py # python impl
_enrich_paper_identifiers.py # python impl
_doilist.py # python impl
paper-count # bash wrapper
paper-monitor-ingest-dir # bash wrapper
find-citing-papers # bash wrapper
enrich-paper-identifiers # bash wrapper
doilist # bash wrapper
README.md
The leading-underscore Python files are private impls — invoke the
wrappers, not the .py files directly.