CLI reference
The researcher-profiles package installs a single console script, rp. Run
rp <subcommand> --help for per-command usage.
rp <subcommand> [options]| Subcommand | Purpose | Extra required |
|---|---|---|
index | Build or update a profile’s embedding index | [vectors,st] |
export-embeddings | Write the servable flat embedding form (embeddings/) from the sqlite index | [vectors,st] |
export | Render a profile as one text blob (plus metadata) for a knowledge base | core |
search | Query a profile’s embedding index | [vectors,st] |
rank-works | Rank new candidate works against a profile’s embedding vector | [vectors,st] |
schema | Export JSON Schema files for the profile format | core |
graph | Build or inspect the derived profile graph (coauthor / COI / advising edges) | core |
db | Push, pull and inspect profiles in a SQL profile store | [sql] |
push | Upload a built profile directory to a remote API server | [client] |
render | Render index.html into a profile folder and refresh its manifest (in place) | core |
publish | Write the static site one audience may see into a folder any sync tool can upload as is | core |
site | Write the collection files (index.json, by-rid.json, SKILL.md, sitemap.xml, …) for a set of profiles | core |
install | Pull profiles from a registry into the local cache | [client] |
seek | Print the local path of an installed profile | core |
list | List profiles in the local cache | core |
listr | List profiles available on a registry | [client] |
login | Log in to a profile server and store a push key | [client] |
logout | Forget the stored login | core |
whoami | Show who the stored login is, if the server says | core |
skill | Print or install the consumer skill for reading published profiles | core |
where | Print the profile directory for a rid or a slug | core |
mint-local-id | Mint a local: rid for a researcher who has no ORCID | core |
validate | Validate a profile directory against the JSON Schema spec | core |
manifest | Show or regenerate a profile’s manifest | core |
sign | Sign a profile’s profile.jsonld with a key_signature proof | [signing] |
sign-verify | Verify the key_signature proof(s) on a profile.jsonld against a JWK Set | [signing] |
The table above lists every verb the parser registers. Every row has a dedicated section below with its flags, exit codes, and a worked example.
The HTTP server has a separate entry point, python -m researcher_profiles.api;
see the API reference.
Naming a profile
Section titled “Naming a profile”Every verb that takes a profile argument accepts three spellings of it:
| Spelling | Example |
|---|---|
| A path to a profile directory | ./profiles/jane-doe |
| A rid | 0000-0002-1825-0097 |
| A directory slug in the local cache | jane-doe |
A path that exists on disk always means itself; only a name that is nothing on
disk is looked up in the cache, so a local directory is never shadowed by a
same-named entry in the cache. --root DIR names the cache to look in, and
otherwise the usual order applies: $RESEARCHER_PROFILES_ROOT, else
~/researcher-profiles. A name that resolves to nothing exits 2 and prints
the root it searched.
This is why rp install jane-doe and rp index jane-doe fit together: install
prints the slug, and every later verb takes it.
Build (or incrementally update) the embedding index for a profile. The index is
written to <profile>/.cache/embeddings.sqlite.
rp index <profile> [--root DIR] [--force] [--backend SPEC]| Argument / flag | Description |
|---|---|
profile | A profile directory, a rid, or a slug (see Naming a profile). |
--root DIR | Profiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles. |
--force | Drop and rebuild the index from scratch. |
--backend SPEC | Backend spec, e.g. st:all-MiniLM-L6-v2. Defaults to the profile’s recorded backend or the package default. |
Prints a one-line report:
backend=st:all-MiniLM-L6-v2 added=4 updated=0 skipped=0 removed=0 duration=5.17sRequires the [vectors,st] extras.
export-embeddings
Section titled “export-embeddings”Write the servable flat embedding form (<profile>/embeddings/) from the
sqlite index at <profile>/.cache/embeddings.sqlite. The flat form is what a
static or dynamic server ships; the sqlite form is a local build artifact.
rp export-embeddings <profile> [--root DIR]| Argument | Description |
|---|---|
profile | A profile directory, a rid, or a slug (see Naming a profile). |
--root DIR | Profiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles. |
$ rp export-embeddings $(rp where jane-doe)wrote embeddings/jane-doe.bin backend=st:all-MiniLM-L6-v2 count=42 dim=384 dropped=3When there is no sqlite index, or no public row survives the privacy tier
filter, it prints no flat form written and exits 0. Any stale flat form
on disk is removed and hasEmbeddingIndex is left false. A name that resolves to no
directory at all exits 2; a directory that exists but holds no index takes
the same no-op path as an empty one.
Requires the [vectors,st] extras.
search
Section titled “search”Query a profile’s embedding index and print ranked hits.
rp search <profile> <query> [--root DIR] [-k N] [--type SOURCE_TYPE] [--json]| Argument / flag | Default | Description |
|---|---|---|
profile | none | A profile directory, a rid, or a slug (see Naming a profile). |
--root DIR | none | Profiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles. |
query | none | Free-text query. |
-k N | 5 | Number of hits to return. |
--type SOURCE_TYPE | none | Filter by source type: paper_summary, paper_abstract, expertise, soul, grant, cv, or web. |
--json | off | Emit the hits as a JSON array instead of the text listing. |
Each hit prints its score, source, chunk index, section, and a text preview:
[0.595] expertise:expertise#1 Region set analysis ## Region set analysis Jane developed methods for enrichment analysis ...Requires the [vectors,st] extras.
rank-works
Section titled “rank-works”Rank new candidate works against a profile’s embedding vector: the “what’s
new that fits this researcher” query. Needs a built embedding index
(rp index).
rp rank-works <profile> [--root ROOT] [--since YYYY-MM-DD] [--openalex] [--works FILE] [-k N] [--kind {centroid,summary,expertise}] [--threshold X] [--exclude-types T1,T2] [--all-types] [--json]| Argument / flag | Default | Description |
|---|---|---|
profile | none | A profile directory, a rid, or a slug. |
--root ROOT | none | Profiles root, used to resolve a rid/slug. |
--since YYYY-MM-DD | 30 days back | Earliest publication date, for --openalex. |
--openalex | off | Fetch candidates from the OpenAlex API. Needs the [client] extra. |
--works FILE | none | JSON array of candidate works (raw OpenAlex works or PaperRecord dicts), instead of --openalex. |
-k N | 10 | Number of ranked works to return. |
--kind {centroid,summary,expertise} | centroid | Which profile vector to rank against. |
--threshold X | none | Drop works scoring below X. |
--exclude-types T1,T2 | a default deposit-type set | OpenAlex work types to drop from --openalex candidates (software, book, etc.). |
--all-types | off | Keep every work type; disables the deposit-type filter. |
--json | off | Emit the ranked works as a JSON array. |
Exactly one of --openalex or --works is required. --openalex reads the
OpenAlex API key from the OPENALEX_API_KEY environment variable (a free key
from https://openalex.org/settings/api); there is no command-line flag for it,
so it stays out of shell history.
rp rank-works voss-elena --openalex --since 2026-07-01rp rank-works voss-elena --works candidates.json -k 10 --jsonExit codes. 0 ok (including “no candidate works survived ranking”) ·
1 the profile could not be loaded, the profile has no subfields/interests
or OpenAlex work ids to seed a query with, the OpenAlex fetch failed, or the
[vectors,st] extras are missing · 2 neither --openalex nor --works was
given, --works FILE could not be read as JSON, or the index has not been
built (run rp index first).
Requires the [vectors,st] extras; --openalex additionally needs [client].
export
Section titled “export”Render a profile as a single deterministic text blob for a knowledge base, or the whole export bundle (identity, links, the corpus DOI list, the text, and a content hash) as canonical JSON.
rp export <profile> [--json] [-o FILE] [--max-papers N] [--char-budget N] [--no-soul] [--no-expertise-doc] [--explore-base URL] [--profile-url URL] [--allow-nonpublic]| Argument / flag | Description |
|---|---|
profile | A profile directory, a rid, or a slug (see Naming a profile). |
--root DIR | Profiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles. |
--json | Emit the export bundle as canonical JSON instead of the bare markdown. |
-o, --out FILE | Write to FILE instead of stdout. |
--max-papers N | Cap on papers whose text enters the blob (default 40). |
--char-budget N | Soft cap on blob characters (default 120000). Whole paper blocks are dropped; a body is never truncated. |
--no-soul | Omit personality/SOUL.md. |
--no-expertise-doc | Omit personality/expertise.md. |
--explore-base URL | Base URL of a profile browser app, used to build the backlink. No default: without it the bundle carries no explore_url. |
--profile-url URL | Override the published profile URL (default: the document’s url). |
--allow-nonpublic | Export a profile whose visibility is above public. |
rp export $(rp where jane-doe) # the blobrp export $(rp where jane-doe) --json # the bundlerp export $(rp where jane-doe) --json -o jane-doe.export.jsonThe blob is deterministic: two runs against an unchanged directory produce
byte-identical output, which is what lets a knowledge base upsert only when
content_hash changes.
Exit codes. 0 ok · 1 the profile could not be loaded · 4 the profile’s
visibility is above public and --allow-nonpublic was not given; the
message names the flag.
See Export for knowledge bases.
schema
Section titled “schema”Export JSON Schema files for the profile format.
rp schema export <out_dir>rp schema export-wire <out_file>export writes one file per model. export-wire writes a single combined
HTTP wire-contract schema, consumed by rp-ui-lib/scripts/gen-types.mjs to
generate the browser’s TypeScript types.
| Argument | Description |
|---|---|
out_dir (export) | Directory to write <model>.schema.json files into. Created if absent. |
out_file (export-wire) | Output file for the combined wire schema, e.g. rp-ui-lib/schemas/wire.schema.json. |
export writes nine files and prints each path:
wrote schemas/profile_jsonld.schema.jsonwrote schemas/papers_jsonld.schema.jsonwrote schemas/grants_jsonld.schema.jsonwrote schemas/summary_file.schema.jsonwrote schemas/embedding_index.schema.jsonwrote schemas/profile_list.schema.jsonwrote schemas/collection.schema.jsonwrote schemas/topic_index.schema.jsonwrote schemas/profile_export_bundle.schema.jsonrp schema export-wire rp-ui-lib/schemas/wire.schema.jsonSee the schema reference.
validate
Section titled “validate”Validate a profile directory against the JSON Schema spec: schema conformance plus manifest agreement. Core; no extra required.
rp validate <profile> [--root DIR] [--json]| Argument / flag | Description |
|---|---|
profile | A profile directory, a rid, or a slug (see Naming a profile). |
--root DIR | Profiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles. |
--json | Output the report as JSON instead of text. |
rp validate $(rp where voss-elena)rp validate ./profiles/voss-elena --jsonA pass with undeclared JSON-LD terms prints a warning banner on stderr; that is not a conformance failure.
Exit codes. 0 conformant · 4 a conformance violation. Re-read the
violations printed, or fetch them keyed by JSON pointer with --json.
Build (or inspect) the derived profile graph (coauthor, COI, and advising edges) over every profile in a store.
rp graph build [--root ROOT] [--json]| Argument / flag | Description |
|---|---|
--root ROOT | Profiles root to scan. Default: the resolved cache. |
--json | Emit the build summary as JSON. |
Writes <root>/.cache/graph.sqlite.
$ rp graph build --root ~/researcher-profilesbuilt graph over 12 profiles: 34 nodes, 58 edges -> /home/jane/researcher-profiles/.cache/graph.sqliteExit codes. 0 ok · 2 an unknown graph subcommand was given (build
is the only one today).
Push, pull and inspect profiles in a SQL profile store.
The store is a peer backend: a profile in it is a profile,
and rp db pull reproduces the directory byte for byte. Pick one backend per
deployment. Nothing should read the same profile from a directory and from the
store at once.
rp db init [--database-url URL] [--json]rp db push <ref>... | --all [--database-url URL] [--root DIR] [--include-binary] [--json]rp db pull <rid|slug> [--database-url URL] [--to DIR] [--with-build] [--json]rp db list [--database-url URL] [--json]rp db rm <rid|slug> [--database-url URL] [--json]| Option | Description |
|---|---|
--database-url | Store URL. Default: $RESEARCHER_PROFILES_DATABASE_URL. |
--root (push) | Profiles root to read from. Default: the resolved cache. |
--include-binary (push) | Store binary artifact bodies too (.cache/embeddings.sqlite can be tens of MB). |
--to (pull) | Destination directory. Default: ./<slug>. |
--with-build (pull) | Also write the build sidecar, which is never part of the published record. |
The URL resolves, highest precedence first: --database-url, then
$RESEARCHER_PROFILES_DATABASE_URL. There is no built-in default, because a guessed
database is a whole profile store written somewhere nobody meant. A command
with no URL exits 2 (invalid usage). Every command prints which database it
acted on.
$ export RESEARCHER_PROFILES_DATABASE_URL="postgresql://user@host/db"$ rp db initcreated the rp_* tables on postgresql://user@host/db$ rp db push --alljane-doe 0000-0002-1825-0097john-smith 0000-0004-4600-113Xpushed 2 profile(s) to postgresql://user@host/db$ rp db pull jane-doe --to ./pulledpulled jane-doe (0000-0002-1825-0097) from postgresql://user@host/db -> pulledExit 2 when a reference matches nothing in the store or when no database URL
is configured, 1 when at least one profile failed to push.
Upload a built profile directory to a remote API server
(PUT /api/v1/profiles/{slug}). The directory’s contents are tarred
(dotfiles like .archive/ excluded) and swapped into place atomically on the
server; the profile is immediately listable, and matchable when it includes a
built .cache/embeddings.sqlite.
Every push first diffs the server’s manifest against the manifest this push
would commit, and refuses to delete anything the server holds unless you pass
--force. That includes a push whose manifest merely shrinks: fewer entries
means fewer artifacts a reader can fetch, even when no single file was named
for deletion. Files the archive builder leaves out (withheld fulltext, anything
under the retired cache/ directory, paths outside the spec) are reported on
stderr.
A push also refuses outright when the local manifest names files this directory
does not have. That is the signature of a partial copy of the profile, not of
drift to be tidied up, and re-indexing the server from an incomplete directory
is how artifacts get deleted. Use rp where to see which root is in
force. Every command that resolves a slug or rid now prints the directory it
resolved to on stderr, and warns when the same slug also exists under another
root in the resolution order.
rp push <profile> [--root DIR] [--url BASE_URL] [--slug SLUG] [--token TOKEN] [--dry-run] [--force] [--only PATH ...] [--merge | --prune] [--include-fulltext] [--json]| Option | Description |
|---|---|
profile | A built profile directory, a rid, or a slug (see Naming a profile); the directory must contain profile.jsonld. |
--root DIR | Profiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles. |
--url | Server base URL, e.g. http://localhost:8109. Default: the server you ran rp login against. |
--slug | Target slug on the server. Default: the directory name. |
--token | Bearer token. Default: RESEARCHER_PROFILES_TOKEN, else the stored login’s key for that server. |
--dry-run | Print what the push would add, change, remove, and keep, then stop. Uploads nothing. |
--force | Push even though it removes files the server holds. |
--only PATH ... | Send only these profile-relative files. profile.jsonld travels too, but its manifest is taken from the server, so nothing else changes. Inline sections (name, expertise) still come from the local document; to change one field only, use the server’s metadata edit endpoint (PATCH /profiles/{slug}/metadata). Implies --merge. |
--merge | Keep every server-side file this push does not carry. |
--prune | Delete every server-side file this push does not carry, including withheld fulltext and the index. Cannot combine with --only or --merge. |
--include-fulltext | Also upload sources/papers/ extracted fulltext. Off by default. |
--json | Emit the server’s summary as JSON (with --dry-run, the whole plan). |
Modes. With no mode flag the push is a replace: the server ends up with
what the archive carries, except that withheld fulltext and the sqlite index
are kept when the archive carried none of them. --only and --merge select
merge: everything not sent stays. --prune selects prune: everything not
sent goes. See the API reference for the
server-side rules.
$ rp push profiles/jane-doe --url http://localhost:8109pushed jane-doe: +12 ~3 -0 (kept 2) name=Jane A. Doe, level=lite, indexed=True
$ rp push profiles/jane-doe --only sources/papers.jsonld --dry-runsource: /home/me/researcher-profiles/jane-doetarget: http://localhost:8109/api/v1/profiles/jane-doe (mode: merge)dry run: push jane-doe -> http://localhost:8109 (update, merge)manifest: 11 entries on server -> 11 after push (0 removed) added 0 changed 1 works 1 unchanged 0 removed 0 kept 10 citations 1, expertise 1, paper_fulltext 2, paper_summary 5, soul 1 respliced 0changed (1): sources/papers.jsonldrespliced counts entries the server keeps that the local manifest no
longer lists. The server adds them back, so they survive — but only a server
advertising the manifest_splice feature (see
GET /api/v1/capabilities) does that. Against
one that does not, the same paths are counted as removed and the push
refuses. A push also asks for the server’s push_modes before it writes, so
--merge or --prune against a server that predates ?mode= is a refusal
rather than a silent replace.
After a real push, the server’s own post-commit count is printed, and a count that fell is a warning on stderr:
$ rp push profiles/jane-doe --only sources/papers.jsonldpushed jane-doe: +0 ~1 -0 (kept 10) name=Jane A. Doe, level=lite, indexed=Trueserver now holds 11 artifacts: citations 1, expertise 1, paper_fulltext 2, paper_summary 5, soul 1, works 1Pushing with an API key. On a server with
per-part access, a key that may
not replace whole profiles may still push to a profile it may write. The server
compares the push with the profile it replaces, part by part, and lands it only
if every part it changes is Write in the key’s table. Otherwise it writes
nothing and rp push names what is in the way:
$ rp push profiles/jane-doe --url http://localhost:8109 --token <key>push refused [insufficient-access]: this key may not change every part this push touches needs Write on: summary next: --only <file> to send only parts you may write, or have the account holder widen this key's accessA change no part covers (what the public sees, a field such as
provenance_note) is listed on its own needs "Replace whole profiles" for:
line: only a key with that switch may make it. rp push --only <file> sends one file and keeps the
rest, so a key with Write on paper_summary can update one summary. The
server says what the key’s table holds; a host plugin for rp may print it.
Refusals. Every refusal is one push refused [<code>]: <what> line, then any
paths or parts involved, then a next: line with the flag or command to run.
The codes are would-remove, manifest-shrink, partial-copy, legacy-cache,
mode-unsupported, mode-unknown, and insufficient-access. In Python they
are PushRefused.code, .details, and .fix.
Exit codes. 0 ok · 1 the server could not be reached or would not
answer, or refused the push part by part (403 insufficient_access) · 2 no server URL resolved, the directory is unreadable, the push
would remove server files (or shrink the manifest) and --force was not given,
the local manifest names files that are not on disk, --prune was combined
with --only/--merge, the server does not support the requested mode, or the
target refused the upload. See the
API reference for validation rules.
render
Section titled “render”Render a single profile’s index.html in place (at public), and refresh its
manifest (hasPart / subjectOf in profile.jsonld). A profile folder is not
itself safe to upload: to deploy, use publish.
rp render <profile> [--root DIR] [--base-url URL] [--no-index]| Argument / flag | Default | Description |
|---|---|---|
profile | none | Path to a single profile directory. |
--base-url URL | none | Base URL used when building links in the rendered page. |
--no-index | off | Add noindex directives so crawlers skip the rendered page. |
Exit code 2 (with a message on stderr) when the profile directory cannot be found.
publish
Section titled “publish”Write the static tree one audience may see into an output folder. For each
profile that audience may see, profiles/<slug>/ gets a profile.jsonld with
every section above the audience removed, only the files the audience may
read, and an index.html and embeddings/ built for the audience. The
collection files (as site writes them) are built for the same
audience. Upload is a separate step: the folder needs no filtering.
rp publish <profiles> --out DIR [--who {public,limited,private}] [--root DIR] [--base-url URL] [--no-index] [--now ISO8601] [--change-audience] [--dry-run] [--json]| Argument / flag | Default | Description |
|---|---|---|
profiles | none | A profiles root, or one profile (a directory, a rid, or a slug). |
-o, --out DIR | none | Output folder. Required. Must be new, empty, or a previous rp publish output. |
--who TIER | public | The audience. limited and private exports also get noindex pages and a disallow-all robots.txt. private prints a warning: it holds what only the owner may see. |
--base-url URL | none | Base URL for canonical links, sitemap.xml and robots.txt. |
--no-index | off | Add noindex directives. |
--now ISO8601 | now | Pin timestamps for deterministic output. |
--change-audience | off | Allow --who to be wider than the audience the folder was last published for (recorded in .rp-publish.json). Without it, widening is refused; narrowing is always allowed. |
--dry-run | off | Print, per profile, what ships and what is withheld and why, plus skipped profiles. Writes nothing. |
--json | off | Print the same as data. |
A re-run into the same folder removes files the previous run wrote and this one does not, so a tier tightened since the last export disappears.
rp publish ~/researcher-profiles --out ./_siteaws s3 sync ./_site s3://my-bucket/ --deleteExit code 1 when a profile’s derivedFrom chain does not resolve (a
restriction that cannot be computed is refused, not guessed) or the output
folder holds files rp publish did not write. Exit code 2 when the profiles
folder cannot be found.
Write the collection files for a set of profiles into an output directory:
index.json, by-rid.json, SKILL.md, style.css, sitemap.xml, and the
rest, at public. site produces the collection-level index only; to build a
deployable folder, use publish, which includes these files.
rp site <profiles_dir> --out DIR [--base-url URL] [--no-index] [--now ISO8601]| Argument / flag | Default | Description |
|---|---|---|
profiles_dir | none | Root directory containing the profile subdirectories. |
-o, --out DIR | none | Output directory for the collection files. Required. |
--base-url URL | none | Base URL for sitemap.xml and robots.txt. |
--no-index | off | Add noindex directives. |
--now ISO8601 | now | Pin timestamps for deterministic output. |
Any warnings are printed to stderr. Exit code 2 (with a message on stderr) when the profiles directory cannot be found.
install
Section titled “install”Pull one or more profiles from a registry into the local cache
($RESEARCHER_PROFILES_ROOT, default ~/researcher-profiles).
rp install <slug> [<slug> ...] [--url URL] [--root ROOT] [--token TOKEN] [--force] [--json]| Argument / flag | Default | Description |
|---|---|---|
slug | none | One or more profile slugs to install. |
--url URL | $RESEARCHER_PROFILES_REGISTRY_URL, else the rp login server | Registry base URL(s), comma-separated. |
--root ROOT | $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles | Local profiles root. |
--token TOKEN | $RESEARCHER_PROFILES_TOKEN, else the stored login’s key | Bearer token, for a private registry. |
--force | off | Re-download even if already cached. |
--json | off | Emit one JSON record per requested slug. |
$ rp install voss-elena jane-doe --url https://profiles.example.orginstalled voss-elena (Elena Voss, level=lite, no fulltext) -> /home/jane/researcher-profiles/voss-elenavoss-elena: already installed at ... (--force to replace)Exit codes. 0 every slug installed (or already present) · 2 at least
one slug failed (a bad registry, a missing token, or a slug the registry does
not have); the command keeps going and reports each failure, then exits 2
if any occurred.
Requires the [client] extra.
Print the local path of an already-installed profile, by directory slug.
rp seek <slug> [--root ROOT] [--json]| Argument / flag | Description |
|---|---|
slug | Directory slug of an installed profile. |
--root ROOT | Local profiles root. |
--json | Emit {slug, path, root} as JSON. |
rp seek voss-elenacat $(rp seek voss-elena)/profile.jsonldseek takes a directory slug only. To resolve an ORCID or a rid, use
where.
Exit codes. 0 ok · 2 no such slug is installed in the resolved root.
List the profile slugs present in the local cache. Core; no extra required.
rp list [--root ROOT] [--json]| Argument / flag | Description |
|---|---|
--root ROOT | Local profiles root. |
--json | Emit {root, profiles} as JSON. |
rp listrp list --root ~/work/som/profilesAn empty cache prints no profiles installed in <root> (to stdout in text
mode) plus, on stderr, which root was used and how to populate it. Exits 0
either way.
List the profiles available on one or more registries.
rp listr [--url URL] [--token TOKEN] [--json]| Argument / flag | Default | Description |
|---|---|---|
--url URL | $RESEARCHER_PROFILES_REGISTRY_URL, else the rp login server | Registry base URL(s), comma-separated. |
--token TOKEN | $RESEARCHER_PROFILES_TOKEN, else the stored login’s key | Bearer token, for a private registry. |
--json | off | Emit one JSON record per remote profile, tagged with which registry answered. |
rp listr --url https://profiles.example.orgRESEARCHER_PROFILES_REGISTRY_URL=https://profiles.example.org rp listr --jsonExit codes. 0 ok · 1 every named registry failed to answer (returning
0 with an empty list would read exactly like “the registry is empty”, which
is not what happened) · 2 no registry URL was given and none is stored.
Requires the [client] extra.
Print the consumer skill (the one that teaches an agent to read a published profile), or install the whole skill tree to a local directory.
rp skill [--print | --install] [--dir DIR]| Flag | Description |
|---|---|
--print | Print SKILL.md to stdout. Default. |
--install | Install the skill files to a local directory. |
--dir DIR | Installation directory. Default: ~/.claude/skills/researcher-profile/. |
rp skillrp skill --installrp skill --install --dir ~/.claude/skills/researcher-profileExit codes. 0 ok · 2 the packaged SKILL.md could not be found (a
broken install).
Resolve a rid or a directory slug to the profile’s local directory. A
directory name is a display handle, never an identity; where is how you go
from either one to a path.
rp where <ref> [--root ROOT] [--json]| Argument / flag | Description |
|---|---|
ref | A rid (ORCID or local:...) or a directory slug. |
--root ROOT | Profiles root. |
--json | Emit {ref, path, root} as JSON. |
rp where voss-elenarp where 0000-0002-1825-0097Exit codes. 0 ok · 2 nothing in the resolved root matches ref.
mint-local-id
Section titled “mint-local-id”Mint a local: rid for a researcher who has no ORCID.
rp mint-local-id --name NAME| Flag | Description |
|---|---|
--name NAME | The researcher’s name. Required. |
$ rp mint-local-id --name "Elena Voss"local:elena-voss-4a2fd1Exit 0 always; there is no failure path.
manifest
Section titled “manifest”Show, regenerate, or check a profile’s manifest: the hasPart / subjectOf
entries in profile.jsonld that enumerate every artifact the directory
contains.
rp manifest <profile> [--root DIR] [--write] [--force] [--check] [--json]| Flag | Description |
|---|---|
| (none) | Print one line per manifest entry (role and contentUrl). |
--write | Regenerate the manifest by walking the directory and store it back into profile.jsonld. Prints the diff first and refuses to drop entries. |
--force | With --write, write even though the new manifest drops entries. |
--check | Exit 4 when the recorded manifest disagrees with what is on disk. |
--json | Emit the manifest entries and any drift as JSON (with --write, also before, after, added, dropped). |
Drift is reported in both directions: files on disk that the manifest does not
list (manifest_unlisted), and manifest entries whose file is gone
(manifest_stale). They want opposite actions. An unlisted file wants the
manifest regenerated. A dangling entry may mean this directory is a partial
copy of the profile, and regenerating there deletes the missing artifacts for
good — so --write names every entry it would drop and refuses without
--force.
$ rp manifest profiles/jane-doesoul personality/SOUL.mdexpertise personality/expertise.mdworks sources/papers.jsonldpaper_summary sources/summaries/doe2019methods.summary.md
$ rp manifest profiles/jane-doe-partial --writemanifest: 141 -> 88 entries (+0 -53) would drop: sources/papers/doe2016example.md ...refusing to drop 53 entries; re-run with --force if these files are really gone.Sign a profile’s profile.jsonld with a key_signature proof (detached JWS
over an Ed25519 key) and (re)write the well-known JWK Set beside it.
rp sign <profile> [--root DIR] [--key KEY] [--base-url BASE_URL] [--gen-key]| Flag | Default | Description |
|---|---|---|
profile | none | A profile directory or its profile.jsonld, a rid, or a slug (see Naming a profile). |
--key KEY | <profile>/.keys/signing.pem | Ed25519 private-key PEM. Generated (mode 0600) if absent. |
--base-url BASE_URL | the document’s url | Published base URL, used to build verificationMethod (.../.well-known/...#kid). |
--gen-key | off | Generate a fresh signing key even if one already exists. |
rp sign $(rp where voss-elena)rp sign ./profiles/voss-elena --base-url https://profiles.example.org/voss-elenaThe private key lives at <profile>/.keys/signing.pem; .keys/ is always
excluded from a publish (see Privacy), so keep it
there rather than moving it into the published tree. A document already
carrying a key_signature proof has that one proof replaced; other proof
kinds are left alone.
Exit codes. 0 ok · 2 no profile.jsonld at the given path, no base
URL resolved (pass --base-url or set the document’s url), or the document
fails schema validation before signing (refuses to sign an invalid document).
Requires the [signing] extra.
sign-verify
Section titled “sign-verify”Verify the key_signature proof(s) on a profile.jsonld against a JWK Set.
rp sign-verify <profile> [--root DIR] [--keys KEYS]| Flag | Default | Description |
|---|---|---|
profile | none | A profile directory or its profile.jsonld, a rid, or a slug (see Naming a profile). |
--keys KEYS | <profile>/.well-known/researcher-profile-keys.json | JWK Set path. |
rp sign-verify $(rp where voss-elena)rp sign-verify ./profiles/voss-elena --keys ./keys/researcher-profile-keys.jsonExit codes. 0 at least one proof verifies · 1 a proof is present but
none verifies · 2 no profile.jsonld at the given path, or no JWK Set to
check against.
Requires the [signing] extra.
Log in to a profile server from the command line and store a login that
renews itself. rp reads the server’s OAuth metadata
(GET /.well-known/oauth-authorization-server) and signs in as the registered
OAuth client rp-cli, asking for the profiles:push scope, in one of two
ways:
- In your browser (the default).
rpopens your browser at the server’s sign-in page and listens for the answer on a one-time address on this computer,http://127.0.0.1:<random port>/callback, protected with PKCE (RFC 8252). You sign in and press Allow; the command finishes by itself. - With a code (
--device).rpprints a link and a short code, and polls until you approve the request in a browser on any device (RFC 8628).rppicks this on its own over SSH (SSH_CONNECTIONorSSH_TTYset), on Linux with no display (noDISPLAYorWAYLAND_DISPLAY), when it cannot open a browser, and when the server offers no browser sign-in, as long as the server offers a login code. Without one,rpprints the sign-in link instead and waits for you to paste back the address the browser ends on.
The login code is snapgate’s shared OAuth client, vendored inside rp-sdk; it
needs only the Python standard library.
See Command-line login for the wire protocol.
rp login [server] [--label NAME] [--device] [--no-browser] [--json]| Argument / flag | Default | Description |
|---|---|---|
server | the server most recently logged in to | Server base URL, e.g. https://profiles.example.org. |
--label NAME | the machine’s hostname | Name for this machine on the server’s approval page and its list of logins (“rp CLI on NAME”). |
--device | off (automatic over SSH or with no display) | Approve with a link and code on any device. |
--no-browser | off | Do not open a browser here. Open the printed link in a browser on any computer; when it ends on a page that does not load (127.0.0.1), paste that page’s address back and press Enter. |
--json | off | Emit {url, orcid, name} as JSON. |
$ rp login https://profiles.example.orgOpened your browser to sign in. If it did not open, visit:
https://profiles.example.org/oauth/authorize?response_type=code&client_id=rp-cli&...
Waiting for you to sign in...logged in to https://profiles.example.org as Jane Doe; login stored in ~/.config/researcher-profiles/credentials.json$ rp login https://profiles.example.org --deviceOpen this link in your browser to approve the login:
https://profiles.example.org/device?code=WQTX-9F4K
Code: WQTX-9F4KWaiting for approval...logged in to https://profiles.example.org as Jane Doe; login stored in ~/.config/researcher-profiles/credentials.jsonThe login is written to ~/.config/researcher-profiles/credentials.json with
mode 0600 (or under $XDG_CONFIG_HOME when that is set), one login per
server. Each holds an access token that lives about an hour, a refresh token,
when the access token expires, where to renew it, and who you are:
{"version": 1, "logins": {"https://profiles.example.org": { "server": "https://profiles.example.org", "issuer": "https://profiles.example.org", "client_id": "rp-cli", "token_endpoint": "https://profiles.example.org/oauth/token", "access_token": "...", "access_expires_at": 1767225600, "refresh_token": "...", "extra": {"orcid": "0000-0002-1825-0097", "name": "Jane Doe"}, "ended_at": null}}}Each login is offered only to the server it came from, and it can upload only the
profiles you own or edit. Commands renew the access token on their own when it
has expired, and once more if the server answers 401; each renewal saves the
new refresh token (the server issues a new one every time) with an atomic
write under a lock, so two rp commands at once do not trip over each other.
Once it is stored, push, rp install, rp listr and
whoami need no flags at all. A server SHOULD list the login
where it lists your credentials (“rp CLI on
A credentials.json written by an older rp is no longer read: commands say
it is from an older version and to run rp login, which replaces it.
A server that does not offer
command-line login
publishes no OAuth metadata (or none naming a device endpoint), and the
command fails at once with
does not offer command-line login. If you decline the request in the
browser, or it expires before you approve it, the command stops and says so;
run rp login again to start over. If the login later expires (unused too
long) or is revoked, commands stop with one sentence, rp: Your rp login for <server> has ended (signed out, revoked, or unused too long). Run rp login again., and the file keeps a note of when and why it ended.
Exit codes. 0 ok · 1 the server refused, you declined, the request expired or timed out, or the server was unreachable
· 2 no server URL was given and none is stored.
Needs no extra. When the server’s metadata lists a whoami_endpoint, rp
asks it who you are and names you in the message.
logout
Section titled “logout”Sign out: revoke a stored login at the server
(RFC 7009 revocation_endpoint),
then delete it. With no argument, the server most recently logged in to.
rp logout [server]$ rp logoutlogged out of https://profiles.example.org; removed it from /home/jane/.config/researcher-profiles/credentials.jsonIf the server cannot be reached, or offers no revocation endpoint, the login is
deleted anyway and rp says to revoke “rp CLI” in your account settings there.
Exits 0 whether or not a login was there; with nothing stored it prints
not logged in (nothing stored).
whoami
Section titled “whoami”Ask a server who the stored token belongs to. rp calls the whoami_endpoint
the server lists in its OAuth metadata (see the spec’s
command-line login); a
server that lists none does not say.
rp whoami [--url BASE_URL] [--token TOKEN] [--json]| Option | Default | Description |
|---|---|---|
--url | the stored login’s server | Server base URL. |
--token | RESEARCHER_PROFILES_TOKEN, else the stored login’s access token (renewed if expired) | Bearer token. |
--json | off | Emit the server’s identity record as JSON. |
$ rp whoamihttps://profiles.example.org: Jane Doe (0000-0002-1825-0097)can push: jane-doe 0000-0002-1825-0097 ownerThe can push list appears when the server’s answer has a profiles list.
Exit codes. 0 ok · 1 the server rejected the token, could not be
reached, or lists no whoami_endpoint · 2 nothing is logged in and no
--url/--token was given.
Needs no extra.