Skip to content

CLI reference

The researcher-profiles package installs a single console script, rp. Run rp <subcommand> --help for per-command usage.

rp <subcommand> [options]
SubcommandPurposeExtra required
indexBuild or update a profile’s embedding index[vectors,st]
export-embeddingsWrite the servable flat embedding form (embeddings/) from the sqlite index[vectors,st]
exportRender a profile as one text blob (plus metadata) for a knowledge basecore
searchQuery a profile’s embedding index[vectors,st]
rank-worksRank new candidate works against a profile’s embedding vector[vectors,st]
schemaExport JSON Schema files for the profile formatcore
graphBuild or inspect the derived profile graph (coauthor / COI / advising edges)core
dbPush, pull and inspect profiles in a SQL profile store[sql]
pushUpload a built profile directory to a remote API server[client]
renderRender index.html into a profile folder and refresh its manifest and .publishignore (in place)core
siteWrite the collection files (index.json, by-rid.json, SKILL.md, sitemap.xml, …) for a set of profilescore
installPull profiles from a registry into the local cache[client]
seekPrint the local path of an installed profilecore
listList profiles in the local cachecore
listrList profiles available on a registry[client]
loginLog in to a profile server and store a push key[client]
logoutForget the stored logincore
whoamiShow who the stored login is and what it may push[client]
skillPrint or install the consumer skill for reading published profilescore
wherePrint the profile directory for a rid or a slugcore
mint-local-idMint a local: rid for a researcher who has no ORCIDcore
validateValidate a profile directory against the JSON Schema speccore
manifestShow or regenerate a profile’s manifestcore
signSign a profile’s profile.jsonld with a key_signature proof[signing]
sign-verifyVerify the key_signature proof(s) on a profile.jsonld against a JWK Set[signing]
agentAgent credential and identity commands[client]
profileAgent profile editing (pull, diff, push, visibility)[client]

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.

Every verb that takes a profile argument accepts three spellings of it:

SpellingExample
A path to a profile directory./profiles/jane-doe
A rid0000-0002-1825-0097
A directory slug in the local cachejane-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 / flagDescription
profileA profile directory, a rid, or a slug (see Naming a profile).
--root DIRProfiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles.
--forceDrop and rebuild the index from scratch.
--backend SPECBackend 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.17s

Requires the [vectors,st] extras.


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]
ArgumentDescription
profileA profile directory, a rid, or a slug (see Naming a profile).
--root DIRProfiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles.
Terminal window
$ rp export-embeddings $(rp where jane-doe)
wrote embeddings/jane-doe.bin backend=st:all-MiniLM-L6-v2 count=42 dim=384 dropped=3

When 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.


Query a profile’s embedding index and print ranked hits.

rp search <profile> <query> [--root DIR] [-k N] [--type SOURCE_TYPE] [--json]
Argument / flagDefaultDescription
profilenoneA profile directory, a rid, or a slug (see Naming a profile).
--root DIRnoneProfiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles.
querynoneFree-text query.
-k N5Number of hits to return.
--type SOURCE_TYPEnoneFilter by source type: paper_summary, paper_abstract, expertise, soul, grant, cv, or web.
--jsonoffEmit 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 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]
[--mailto EMAIL] [--exclude-types T1,T2] [--all-types]
[--json]
Argument / flagDefaultDescription
profilenoneA profile directory, a rid, or a slug.
--root ROOTnoneProfiles root, used to resolve a rid/slug.
--since YYYY-MM-DD30 days backEarliest publication date, for --openalex.
--openalexoffFetch candidates from the OpenAlex API. Needs the [client] extra.
--works FILEnoneJSON array of candidate works (raw OpenAlex works or PaperRecord dicts), instead of --openalex.
-k N10Number of ranked works to return.
--kind {centroid,summary,expertise}centroidWhich profile vector to rank against.
--threshold XnoneDrop works scoring below X.
--mailto EMAILnoneOpenAlex polite-pool contact.
--exclude-types T1,T2a default deposit-type setOpenAlex work types to drop from --openalex candidates (software, book, etc.).
--all-typesoffKeep every work type; disables the deposit-type filter.
--jsonoffEmit the ranked works as a JSON array.

Exactly one of --openalex or --works is required.

Terminal window
rp rank-works voss-elena --openalex --since 2026-07-01
rp rank-works voss-elena --works candidates.json -k 10 --json

Exit 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].


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 / flagDescription
profileA profile directory, a rid, or a slug (see Naming a profile).
--root DIRProfiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles.
--jsonEmit the export bundle as canonical JSON instead of the bare markdown.
-o, --out FILEWrite to FILE instead of stdout.
--max-papers NCap on papers whose text enters the blob (default 40).
--char-budget NSoft cap on blob characters (default 120000). Whole paper blocks are dropped; a body is never truncated.
--no-soulOmit personality/SOUL.md.
--no-expertise-docOmit personality/expertise.md.
--explore-base URLBase URL of a profile browser app, used to build the backlink. No default: without it the bundle carries no explore_url.
--profile-url URLOverride the published profile URL (default: the document’s url).
--allow-nonpublicExport a profile whose visibility is above public.
Terminal window
rp export $(rp where jane-doe) # the blob
rp export $(rp where jane-doe) --json # the bundle
rp export $(rp where jane-doe) --json -o jane-doe.export.json

The 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.


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.

ArgumentDescription
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.json
wrote schemas/papers_jsonld.schema.json
wrote schemas/grants_jsonld.schema.json
wrote schemas/summary_file.schema.json
wrote schemas/embedding_index.schema.json
wrote schemas/profile_list.schema.json
wrote schemas/collection.schema.json
wrote schemas/topic_index.schema.json
wrote schemas/profile_export_bundle.schema.json
Terminal window
rp schema export-wire rp-ui-lib/schemas/wire.schema.json

See the schema reference.


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 / flagDescription
profileA profile directory, a rid, or a slug (see Naming a profile).
--root DIRProfiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles.
--jsonOutput the report as JSON instead of text.
Terminal window
rp validate $(rp where voss-elena)
rp validate ./profiles/voss-elena --json

A 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 / flagDescription
--root ROOTProfiles root to scan. Default: the resolved cache.
--jsonEmit the build summary as JSON.

Writes <root>/.cache/graph.sqlite.

Terminal window
$ rp graph build --root ~/researcher-profiles
built graph over 12 profiles: 34 nodes, 58 edges -> /home/jane/researcher-profiles/.cache/graph.sqlite

Exit 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]
OptionDescription
--database-urlStore 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.

Terminal window
$ export RESEARCHER_PROFILES_DATABASE_URL="postgresql://user@host/db"
$ rp db init
created the rp_* tables on postgresql://user@host/db
$ rp db push --all
jane-doe 0000-0002-1825-0097
john-smith 0000-0004-4600-113X
pushed 2 profile(s) to postgresql://user@host/db
$ rp db pull jane-doe --to ./pulled
pulled jane-doe (0000-0002-1825-0097) from postgresql://user@host/db -> pulled

Exit 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.

rp push <profile> [--root DIR] [--url BASE_URL] [--slug SLUG] [--token TOKEN]
[--include-fulltext] [--json]
OptionDescription
profileA built profile directory, a rid, or a slug (see Naming a profile); the directory must contain profile.jsonld.
--root DIRProfiles root to resolve a rid or slug against. Default: $RESEARCHER_PROFILES_ROOT, else ~/researcher-profiles.
--urlServer base URL, e.g. http://localhost:8109. Default: the server you ran rp login against.
--slugTarget slug on the server. Default: the directory name.
--tokenBearer token. Default: RESEARCHER_PROFILES_TOKEN, else the stored login’s key for that server.
--include-fulltextAlso upload sources/papers/ extracted fulltext. Off by default.
--jsonEmit the server’s summary as JSON.
Terminal window
$ rp push profiles/jane-doe --url http://localhost:8109
pushed jane-doe (Jane A. Doe, level=lite, indexed=True)

Exit codes. 0 ok · 1 the server could not be reached or would not answer · 2 no server URL resolved, the directory is unreadable, or the target refused the upload. See the API reference for validation rules.


Render a single profile’s index.html in place, and refresh the two derived files that must stay in step with the profile on disk: its manifest (hasPart / subjectOf in profile.jsonld) and .publishignore (the list of internal/restricted artifacts a dumb sync must not carry). There is no transform and no separate output tree: the folder is publishable by construction.

rp render <profile> [--root DIR] [--base-url URL] [--no-index]
Argument / flagDefaultDescription
profilenonePath to a single profile directory.
--base-url URLnoneBase URL used when building links in the rendered page.
--no-indexoffAdd noindex directives so crawlers skip the rendered page.

Exit code 2 (with a message on stderr) when the profile directory 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. Deployment is then a dumb sync of the folders. site produces the collection-level index, not per-profile pages (use render for those).

rp site <profiles_dir> --out DIR [--base-url URL]
[--no-index] [--now ISO8601]
Argument / flagDefaultDescription
profiles_dirnoneRoot directory containing the profile subdirectories.
-o, --out DIRnoneOutput directory for the collection files. Required.
--base-url URLnoneBase URL for sitemap.xml and robots.txt.
--no-indexoffAdd noindex directives.
--now ISO8601nowPin 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.


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 / flagDefaultDescription
slugnoneOne or more profile slugs to install.
--url URL$RESEARCHER_PROFILES_REGISTRY_URL, else the rp login serverRegistry base URL(s), comma-separated.
--root ROOT$RESEARCHER_PROFILES_ROOT, else ~/researcher-profilesLocal profiles root.
--token TOKEN$RESEARCHER_PROFILES_TOKEN, else the stored login’s keyBearer token, for a private registry.
--forceoffRe-download even if already cached.
--jsonoffEmit one JSON record per requested slug.
Terminal window
$ rp install voss-elena jane-doe --url https://profiles.example.org
installed voss-elena (Elena Voss, level=lite, no fulltext) -> /home/jane/researcher-profiles/voss-elena
voss-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 / flagDescription
slugDirectory slug of an installed profile.
--root ROOTLocal profiles root.
--jsonEmit {slug, path, root} as JSON.
Terminal window
rp seek voss-elena
cat $(rp seek voss-elena)/profile.jsonld

seek 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 / flagDescription
--root ROOTLocal profiles root.
--jsonEmit {root, profiles} as JSON.
Terminal window
rp list
rp list --root ~/work/som/profiles

An 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 / flagDefaultDescription
--url URL$RESEARCHER_PROFILES_REGISTRY_URL, else the rp login serverRegistry base URL(s), comma-separated.
--token TOKEN$RESEARCHER_PROFILES_TOKEN, else the stored login’s keyBearer token, for a private registry.
--jsonoffEmit one JSON record per remote profile, tagged with which registry answered.
Terminal window
rp listr --url https://profiles.example.org
RESEARCHER_PROFILES_REGISTRY_URL=https://profiles.example.org rp listr --json

Exit 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]
FlagDescription
--printPrint SKILL.md to stdout. Default.
--installInstall the skill files to a local directory.
--dir DIRInstallation directory. Default: ~/.claude/skills/researcher-profile/.
Terminal window
rp skill
rp skill --install
rp skill --install --dir ~/.claude/skills/researcher-profile

Exit 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 / flagDescription
refA rid (ORCID or local:...) or a directory slug.
--root ROOTProfiles root.
--jsonEmit {ref, path, root} as JSON.
Terminal window
rp where voss-elena
rp where 0000-0002-1825-0097

Exit codes. 0 ok · 2 nothing in the resolved root matches ref.


Mint a local: rid for a researcher who has no ORCID.

rp mint-local-id --name NAME
FlagDescription
--name NAMEThe researcher’s name. Required.
Terminal window
$ rp mint-local-id --name "Elena Voss"
local:elena-voss-4a2fd1

Exit 0 always; there is no failure path.


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] [--check] [--json]
FlagDescription
(none)Print one line per manifest entry (role and contentUrl).
--writeRegenerate the manifest by walking the directory and store it back into profile.jsonld.
--checkExit 4 when the recorded manifest disagrees with what is on disk.
--jsonEmit the manifest entries and any drift as JSON.

Drift is reported in both directions: files on disk that the manifest does not list, and manifest entries whose file is gone. A manifest that has silently drifted is worse than no manifest at all, because a consumer trusts it.

Terminal window
$ rp manifest profiles/jane-doe
soul personality/SOUL.md
expertise personality/expertise.md
works sources/papers.jsonld
paper_summary sources/summaries/doe2019methods.summary.md

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]
FlagDefaultDescription
profilenoneA profile directory or its profile.jsonld, a rid, or a slug (see Naming a profile).
--key KEY<profile>/.keys/signing.pemEd25519 private-key PEM. Generated (mode 0600) if absent.
--base-url BASE_URLthe document’s urlPublished base URL, used to build verificationMethod (.../.well-known/...#kid).
--gen-keyoffGenerate a fresh signing key even if one already exists.
Terminal window
rp sign $(rp where voss-elena)
rp sign ./profiles/voss-elena --base-url https://profiles.example.org/voss-elena

The 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.


Verify the key_signature proof(s) on a profile.jsonld against a JWK Set.

rp sign-verify <profile> [--root DIR] [--keys KEYS]
FlagDefaultDescription
profilenoneA profile directory or its profile.jsonld, a rid, or a slug (see Naming a profile).
--keys KEYS<profile>/.well-known/researcher-profile-keys.jsonJWK Set path.
Terminal window
rp sign-verify $(rp where voss-elena)
rp sign-verify ./profiles/voss-elena --keys ./keys/researcher-profile-keys.json

Exit 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 the key it mints. The server runs a device-authorization flow: rp asks for a code, prints a link and a short user code, and polls until you approve the request in a browser. See Command-line login for the wire protocol.

rp login [server] [--label NAME] [--no-browser] [--json]
Argument / flagDefaultDescription
serverthe server already logged in toServer base URL, e.g. https://profiles.example.org.
--label NAMEthe machine’s hostnameName shown for this machine on the approval page.
--no-browseroffPrint the link only; do not open a browser.
--jsonoffEmit {url, orcid, name} as JSON.
Terminal window
$ rp login https://profiles.example.org
Open this link in your browser to approve the login:
https://profiles.example.org/cli-auth?code=WQTX-9F4K
Code: WQTX-9F4K
Waiting for approval...
logged in to https://profiles.example.org as Jane Doe; key stored in ~/.config/researcher-profiles/credentials.json

The key is written to ~/.config/researcher-profiles/credentials.json with mode 0600 (or under $XDG_CONFIG_HOME when that is set). It is scoped to the one server it came from, and it carries only push_own, so it can upload the profiles you own or edit and nothing else. Once it is stored, push, rp install, rp listr and whoami need no flags at all.

A server that does not implement the management tier answers 404 on the login request, and the command fails with does not offer command-line login.

Exit codes. 0 ok · 1 the server refused, timed out, or was unreachable · 2 no server URL was given and none is stored.

Requires the [client] extra.


Delete the stored login. Takes no options.

rp logout
Terminal window
$ rp logout
logged out; removed /home/jane/.config/researcher-profiles/credentials.json

Exits 0 whether or not a file was there; with nothing stored it prints not logged in (nothing stored).


Ask a server who the stored token belongs to and which profiles it may push (GET /api/manage/whoami).

rp whoami [--url BASE_URL] [--token TOKEN] [--json]
OptionDefaultDescription
--urlthe stored login’s serverServer base URL.
--tokenRESEARCHER_PROFILES_TOKEN, else the stored login’s keyBearer token.
--jsonoffEmit the server’s identity record as JSON.
Terminal window
$ rp whoami
https://profiles.example.org: Jane Doe (0000-0002-1825-0097)
scopes: push_own
can push:
jane-doe 0000-0002-1825-0097 owner

Exit codes. 0 ok · 1 the server rejected the token or could not be reached · 2 nothing is logged in and no --url/--token was given.

Requires the [client] extra.


Introspect an agent credential: the rpa_ key an assistant holds to edit one profile on one server. Distinct from login, which handles a person’s own rpk_ push key.

rp agent whoami [--host NAME] [--json]
rp agent scopes [--host NAME] [--json]
rp agent config [--host NAME]
SubcommandCallsPrints
whoamiGET /api/manage/agent/whoamiThe agent’s label and handle, its owner, viewer tier, granted and missing scopes, and the profiles it is bound to.
scopesGET /api/manage/agent/scopesThe scope catalog, with the dangerous and default-on flags.
confignothing (local)Which credential resolved, and from where.
OptionDescription
--host NAMEWhich [hosts.<name>] block in credentials.toml to use. Default: the file’s default =, or $RESEARCHER_PROFILES_AUTH_HOST.
--json(whoami, scopes) Emit the server’s response verbatim.

The credential resolves in four steps; the first hit wins (researcher_profiles.agent.resolve_credential):

  1. Explicit key and url arguments passed to resolve_credential in Python. The command line has no --key / --url; it selects a host with --host.
  2. The RESEARCHER_PROFILES_AGENT_KEY and RESEARCHER_PROFILES_API_URL environment variables, both set.
  3. The nearest .env walked up from the working directory, same two names.
  4. ~/.config/researcher-profiles/credentials.toml, which must be mode 0600.

The scope vocabulary is normative in Agent scopes.

Terminal window
$ rp agent whoami
Agent: laptop-assistant (rpa_7f3a91c2b40e)
Owner: Jane Doe (0000-0002-1825-0097)
Tier: restricted
Scopes: profile:history, profile:metadata, profile:narrative, read
Missing: profile:identity, profile:visibility
Profile: jane-doe (editor)

Exit codes. 0 ok · 1 no credential resolved, or the server refused · 2 no subcommand was given.

Requires the [client] extra.


Edit a profile through an agent credential, as a round trip through one local file. The target profile is not a command-line argument: it comes from profile = in the resolved [hosts.<name>] block of credentials.toml.

rp profile pull [-o FILE] [--host NAME]
rp profile diff <file> [--host NAME]
rp profile push <file> [--if-match HASH] [--force] [--dry-run] [--host NAME]
rp profile visibility get|set [--tier TIER] [--host NAME]
OptionDescription
-o, --output FILE (pull)Where to write the document. Default: profile.md.
--if-match HASH (push)Base hash to send instead of the one in the frontmatter.
--force (push)Send no base_hash, so the write is last-writer-wins.
--dry-run (push)Print what would change and send nothing.
--tier TIER (visibility set)public, internal, or restricted. Required for set.
--host NAMEWhich [hosts.<name>] block to use.

The pull document is YAML frontmatter followed by the SOUL narrative as the body. The frontmatter carries slug, rid, base_hash, and every editable metadata field the server returned; the editable set is normative in PATCH /profiles/{slug}/metadata.

Terminal window
$ rp profile pull -o profile.md
Wrote profile.md
base_hash: sha256:6b1c...
$ rp profile diff profile.md
Would change: summary, soul
$ rp profile push profile.md

push sends the frontmatter’s base_hash by default, so an owner edit that landed since the pull returns 409 rather than silently overwriting it. Re-pull and re-apply, or pass --force to overwrite anyway. Changed metadata goes to PATCH /api/v1/profiles/{slug}/metadata and a changed body to PUT /api/v1/profiles/{slug}/soul; visibility is never part of a push, only of rp profile visibility set, and an agent key may narrow a tier but never widen one.

Exit codes. 0 ok · 1 no credential resolved, no profile = configured, the file is missing, or the server refused (including a 403 for a missing scope, which you should report rather than retry) · 2 no subcommand was given, or set was called with no --tier.

Requires the [client] extra.