Personal Assistant Role: Search Email and Notes Together
The Personal Assistant role indexes a Fastmail JMAP mailbox and an Obsidian vault under a single Terraphim role, so one query returns both notes and email ordered by knowledge-graph relevance. This how-to shows the end-to-end setup.
Why this exists
Most "personal AI" tools split your context across silos: one search box for email, another for notes, a third for chat history. Terraphim treats every source as a haystack on the same role, so a single query crosses them. The Personal Assistant role wires up the two most common personal sources -- email (JMAP) and notes (Obsidian) -- with deterministic, sub-millisecond ranking and no cloud round-trip.
Prerequisites
-
A locally built
terraphim-agentwith thejmapfeature. The crates.io binary does not includehaystack_jmap(the dependency is not yet published), socargo install terraphim_agentwill index the Obsidian vault but silently skip JMAP. See Crates Overview for the workspace layout.
To enable the feature in your local checkout (the published crates have it commented out):
# crates/terraphim_middleware/Cargo.toml
haystack_jmap = { path = "../haystack_jmap", version = "1.0.0", optional = true }
# in [features]
jmap = ["dep:haystack_jmap"]
# crates/terraphim_agent/Cargo.toml -- in [features]
jmap = ["terraphim_middleware/jmap"]-
An Obsidian vault on the local filesystem -- any path containing markdown files works. This guide assumes
~/synced/ObsidianVault. -
A Fastmail JMAP access token. Generate one at https://app.fastmail.com/settings/security/tokens with the "Mail" scope. Store it in 1Password (or any secret manager that exposes it via env at runtime) -- never paste it into the role config on disk.
Step 1 -- Add the role to embedded_config.json
Back up first:
Add the role under roles in ~/.config/terraphim/embedded_config.json:
"Personal Assistant": Notes on the choices:
- The Obsidian haystack uses
Ripgrepbecause the vault is just markdown -- no Obsidian-specific service needed.read_only: trueensures the agent never edits notes. - The JMAP haystack
locationdefaults to Fastmail's session URL; override it for other JMAP providers. kg.knowledge_graph_local.pathpoints at the same vault, so the role's knowledge graph is built from your own notes -- this gives the Aho-Corasick matcher synonyms specific to your project vocabulary, which boosts ranking on both notes and email.extra_parameters.limitcaps the JMAP search to 50 hits per query; tune as needed.- The token is not in the JSON. JMAP haystack reads
JMAP_ACCESS_TOKENfrom environment first, then falls back toextra_parameters.access_token. We use the env path so the secret never lands on disk.
Reload the agent's persisted config from the JSON file:
Verify the role appears:
The output should include Personal Assistant (PA) alongside the existing roles.
Step 2 -- Wrapper script for token injection
Because JMAP_ACCESS_TOKEN must be set in the agent's environment for every Personal Assistant query, the cleanest pattern is a small wrapper that uses your secret manager (here, op run from the 1Password CLI) to inject the token at exec time:
Replace VAULT/ITEM with the path to your Fastmail token in 1Password. After this, terraphim-agent-pa behaves exactly like terraphim-agent for the Personal Assistant role; the bare terraphim-agent continues to work for the other roles without paying the 1Password unlock cost.
The token only ever exists inside the running process. Verify nothing leaked to disk:
The grep should return nothing.
Step 3 -- Verify search
Notes-only query (no token needed; terraphim-agent is fine):
Each hit should have a path under your Obsidian vault.
Email query (use the wrapper):
Each hit should have a jmap:///email/<id> URL and the sender's address in the description.
Cross-source query -- a term that appears in both notes and email, e.g. a project name:
You should see notes and emails interleaved, ordered by terraphim-graph rank.
Auto-routing
When you call terraphim-agent search "query" without --role, the agent now scores every configured role's knowledge graph against the query and picks the highest-ranked match. The decision is printed once on stderr:
[auto-route] picked role "Personal Assistant" (score=2, candidates=4); to override, pass --rolestdout is untouched, so --robot and --format json output remain pure JSON. Pass --role "Some Role" to short-circuit auto-routing.
When JMAP_ACCESS_TOKEN is not set, the Personal Assistant's score is multiplied by 0.5 (it loses the JMAP half of its corpus). The role still competes -- a clearly PA-flavoured query like invoice tax still wins over local-only roles when only PA matches.
Troubleshooting
- No email hits, no error -- the warning
JMAP haystack support not enabled. Skipping haystack:in stderr means your binary lacks thejmapfeature. Rebuild from local source per the Prerequisites. - No email hits, no warning -- run the wrapper with
op run --no-maskingonce and confirmJMAP_ACCESS_TOKENis non-empty inside the subshell. If empty, the 1Password reference is wrong. 401 Unauthorizedfrom Fastmail -- the token has been revoked or scoped without "Mail" access. Regenerate at https://app.fastmail.com/settings/security/tokens.- Ranking feels off -- the role's knowledge graph indexes the whole vault on first use; subsequent edits to notes need a
terraphim-agent config reload(which rebuilds the role's KG within ~20 ms). - UTF-8 panic in CLI output -- some snippets containing fancy quotes can trip a known truncation bug at
crates/terraphim_agent/src/main.rs:1414. The search itself succeeds; only the trailing display crashes. Pipe throughhead -n Nto bound the output until the upstream fix lands.
Related
- Command Rewriting How-To -- generic pattern for adding haystacks to any role.
- Architecture -- how roles, haystacks, and the knowledge graph compose.
- Crates Overview -- where
haystack_jmapandterraphim_middlewarelive.