# Rivendell — agent guide

Rivendell is a self-hosted personal contacts graph: people and organisations,
how they are linked, birthdays, notes, and when they were last in touch. It is
built for AI agents to read and write through a plain REST API or MCP.

Base URL: `https://rivendell.jobvo.co` (public internet; a bearer key is the
only guard — keep it secret). OpenAPI: `/api/openapi.json`, Swagger: `/api/docs`.

## Authentication

Every API and MCP call needs `Authorization: Bearer rv_…`. Keys are created in
the web UI (Settings → API keys) or with an admin key via `POST /api/v1/keys`.
Scopes: `read` (GET + read tools), `write` (create/update/delete contacts,
links, notes, interactions), `admin` (manage keys, read the audit log).
`write` implies `read`; `admin` implies both. Every write is audited under the
key's name.

```bash
export RV=https://rivendell.jobvo.co RV_KEY=rv_…
curl -H "Authorization: Bearer $RV_KEY" "$RV/api/v1/auth/me"
```

Errors come back as `{"error": {"code": "...", "message": "...", "details": {}}}`
with the matching HTTP status (401 bad key, 403 missing scope, 404, 409
conflict, 422 invalid input, 429 rate limited).

## Referencing a contact

Anywhere a path or body takes a contact `ref` you may pass:

- the uuid
- `email:someone@example.com`
- `phone:+31612345678` (any formatting; normalised)
- `ext:provider:id` (an external identity, e.g. `ext:linkedin:janedoe`)
- an exact display name (`Frodo Baggins`) — unique match only

## Contact shape

```json
{
  "id": "uuid", "kind": "person|org",
  "display_name": "Frodo Baggins", "given_name": "Frodo", "family_name": "Baggins", "nickname": null,
  "company": "Fellowship", "title": "Ring-bearer",
  "birthday": "1985-03-14" | "--03-14" | null,
  "emails": ["frodo@shire.me"], "phones": ["+31612345678"], "tags": ["hobbit"],
  "channels": [{"id": "...", "kind": "email|phone|url|address|social|other", "label": "work", "value": "...", "is_primary": false}],
  "identities": [{"provider": "linkedin", "external_id": "frodo"}],
  "summary": "one paragraph: who is this", "notes": "running markdown notes",
  "last_contact_at": "2026-10-01T10:00:00+00:00", "cadence_days": 30, "starred": false,
  "attrs": {"city": "Amsterdam"}, "created_at": "...", "updated_at": "...", "deleted_at": null
}
```

Birthday accepts `YYYY-MM-DD`, `--MM-DD` (year unknown), `DD-MM-YYYY`, or
`{"year","month","day"}`. `attrs` is free JSON for anything not modelled.
`cadence_days` means "I want to be in touch every N days" and drives the
overdue list. `last_contact_at` is bumped automatically by interactions.

## REST endpoints (prefix `/api/v1`)

Contacts
- `GET /contacts?q=&tag=&company=&kind=&starred=&overdue=&not_contacted_days=&updated_since=&sort=&limit=&offset=` — search. `q` is full-text + fuzzy over names, company, title, emails, phones, tags, summary. `sort`: relevance | name | -updated | -created | -last_contact | last_contact. Max limit 500. Returns `{items, total, limit, offset, has_more}`; items are brief (add `brief=false` for full).
- `POST /contacts/search` — same filters as a JSON body, plus `tags: [..]` (all must match) and `attr: {..}` (JSONB containment).
- `GET /contacts/lookup?email=|phone=|external=provider:id|name=` — exact lookup (404 when none). Use before creating.
- `POST /contacts` — create. Needs a `display_name` or given/family name or company or an email. Accepts the contact shape plus shortcuts `emails`, `phones`, `urls`, `addresses` (lists of strings or `{value,label,is_primary}`), `tags` (names, created on the fly), `identities` (`"provider:id"` strings).
- `GET /contacts/{ref}` — full contact + `connections`, `recent_interactions`, `notes_list`.
- `PATCH /contacts/{ref}` — partial update. `emails`/`phones`/`urls`/`channels` REPLACE that kind; `add_channels` appends; `tags` replaces, `add_tags`/`remove_tags` adjust; `attrs` merges (null deletes a key); `notes` replaces the running notes text.
- `DELETE /contacts/{ref}` — soft delete (`?purge=true` is permanent). `POST /contacts/{id}/restore`.
- `POST /contacts/{ref}/merge` `{"loser": ref}` — fold a duplicate into this contact.
- `POST /contacts/{ref}/touch` `{"at": "..."}` — set last_contact_at without details.
- `POST /contacts/bulk` `{"items": [...], "match": ["id","identity","email","name"], "dry_run": false}` — upsert up to 2000. On update, lists are ADDED to, scalars replaced. Returns `{created, updated, skipped, results, errors}`.
- `POST /contacts/{ref}/channels` `{"kind","value","label"}`, `DELETE /contacts/{ref}/channels/{id}`
- `POST /contacts/{ref}/tags` `{"tags": [..]}`, `DELETE /contacts/{ref}/tags/{name}`

Graph (edges are stored once; symmetric kinds read both ways)
- `POST /links` `{"from": ref, "to": ref, "kind": "friend", "label": "met at YC", "strength": 1-5, "since": "2019-05", "notes": ""}` — upsert. Kinds: family, partner, parent, child, sibling, friend, colleague, manager, report, client, supplier, investor, founder, introduced_by, knows, other. Direction matters only for parent/child/manager/report/introduced_by/investor/founder (`from` is the parent/manager/introducer/investor/founder OF `to`).
- `POST /links/bulk` `{"items": [...]}`, `GET /links?contact=&kind=`, `GET /links/kinds`, `GET/PATCH/DELETE /links/{id}` (PATCH accepts `swap: true` to reverse), `DELETE /links?a=&b=&kind=`
- `GET /contacts/{ref}/connections?kind=` — direct neighbours with the edge (`direction`: out|in).
- `GET /contacts/{ref}/graph?depth=1..3&max_nodes=300&kinds=friend,family` — ego network `{nodes, edges, truncated}`.
- `GET /graph/path?from=&to=&max_depth=6` — shortest chain of introductions.
- `GET /graph/mutual?a=&b=`, `GET /graph/stats`

Timeline
- `POST /contacts/{ref}/interactions` `{"kind": "meeting|call|message|email|gift|event|other", "summary": "one line", "details": "", "occurred_at": "2026-10-01"}` — log a touchpoint (bumps last_contact_at). `GET /contacts/{ref}/interactions`, `GET /interactions?limit=` (everyone, newest first), `PATCH/DELETE /interactions/{id}`.
- `POST /contacts/{ref}/notes` `{"body": "markdown", "pinned": false}`, `GET /contacts/{ref}/notes`, `PATCH/DELETE /notes/{id}`.

Reminders
- `GET /birthdays?days=30` — upcoming birthdays with `next_birthday`, `days_until`, `turning`.
- `GET /overdue` — contacts past their cadence, most overdue first.

Data
- `GET /export?format=jsonl|json|vcf` — everything (jsonl streams `{"type":"contact"}` lines then `{"type":"link"}` lines).
- `POST /import` — multipart `file` (.vcf / .csv / .json / .jsonl) or JSON `{"format","text","dry_run"}`; upserts with the bulk matching rules.
- `GET /tags`, `PATCH/DELETE /tags/{id}`; `GET /stats`; `GET /audit` (admin); `GET /healthz`.

## MCP

Streamable HTTP at `https://rivendell.jobvo.co/mcp/` (stateless; the same bearer key).

```bash
claude mcp add --transport http rivendell https://rivendell.jobvo.co/mcp/ --header "Authorization: Bearer $RV_KEY"
```

Tools (27): search_contacts, get_contact, lookup_contact, create_contact, update_contact,
delete_contact, merge_contacts, bulk_upsert_contacts, list_tags, stats, link_contacts,
unlink_contacts, update_link, get_connections, get_graph, find_path, mutual_connections,
link_kinds, log_interaction, list_interactions, touch_contact, add_note, list_notes,
update_note, delete_note, upcoming_birthdays, overdue_contacts. Tool docstrings carry the
parameter semantics; they mirror the REST endpoints above.

## Conventions for agents

- Look before you create: `lookup_contact` / `GET /contacts/lookup` by email, then `search_contacts` by name.
- After a real touchpoint, `log_interaction` (one-line summary; details optional). Only `touch` when you have nothing to say.
- Put durable facts in `add_note` (kids' names, allergies, what they're working on). Put identity-ish facts in `attrs`.
- Use `link_contacts` generously: the graph is the point. Add `label` for how/where they know each other.
- `cadence_days` on people worth keeping warm; then `overdue_contacts` tells you who to ping.
- Bulk work goes through `/contacts/bulk` and `/links/bulk` (2000 per call), exports through `/export`.

## Changelog

- 2026-10-10 — initial release: contacts + channels + tags + identities, typed link graph with ego/path/mutual queries, interactions + notes, birthdays/overdue reminders, vCard/CSV/JSON import, JSONL/JSON/vCard export, scoped API keys, MCP server, audit log.
