A local-first LLM interface for roleplay and advanced story writing, with a Claude Code-style terminal UI. Step into a world, play opposite one or many characters, and let a memory-aware engine keep the story coherent across sessions. Runs on local Ollama models or cloud providers (Anthropic, OpenAI, Gemini, Cloudflare Workers AI, AWS Bedrock, and more).
This is a roleplay-only interface — there is no assistant/"chat" mode. Everything is built around characters, worlds, scenes, and continuity: persistent character identities, evolving relationships and trust, tracked world state (location, time, who's present, ongoing events), and cross-session carryover so a world remembers where you left off.
All memory is stored locally in SQLite. Cloud providers are opt-in. Nothing leaves your machine by default.
- Roleplay-first — characters, worlds, scenes, and narrative immersion; no assistant mode.
- Single or multi-character worlds — play one companion or a full cast with a primary voice and supporting characters (
/mode single|multi). - Live world & character authoring — reshape lore, scenes, rules, and characters with
/world set|ruleand/character set|add, then/world saveto keep them as a reusable user world. - Continuity engine — per-session world state (location, time, who's present, ongoing events, mood), rolling summaries, character stances, and pinned/recalled context, assembled within a token budget.
- Cross-session carryover — a world remembers earlier sessions; new sessions seed from the latest snapshot and the prompt notes where you left off (
/recap). - Character-scoped memory — facts and relationships are stored per character, so different roleplays never bleed into one another.
- Evolving relationships — trust shifts with each session's emotional tenor, and the character knows whether you're newly met, close, or estranged.
- Guided onboarding — a fresh start drops you straight into a world picker instead of an empty prompt.
- Many model backends — local Ollama plus Anthropic, OpenAI, Gemini, Cloudflare Workers AI, AWS Bedrock, Nomi, and Kindroid. Switch mid-scene; world, memory, and session stay put.
- Local-first & private — all memory in local SQLite, cloud providers opt-in, encrypted key storage, no telemetry.
CLI emerged from this original project: https://github.com/4tlasX/oread-companion
The goal is to eventually make this the backend for the original GUI which will turn into a desktop app.
OLLAMA_MAX_LOADED_MODELS=2 ollama serve # in a separate terminal
npm install
npm run build
npm link
oreadoread # Terminal UI (default)
oread --api # Terminal UI + API server on :3002
oread --api --no-repl # API server only (headless)
oread --api --port=4000 # Custom port| Command | Description |
|---|---|
/worlds |
Interactive world picker |
/world <id> |
Load a world, start a new session, show recent sessions to resume |
/world |
Show active world and current session |
/world status |
Full world detail — setting, scene, rules, character lineup |
/world set <field> <text> |
Edit the world: lore, scene, narrator, name |
/world rule <add|remove> <text> |
Add or remove a hard rule the characters must obey |
/world save [name] |
Save the current world (with your edits) as a reusable user world |
/mode <single|multi> |
Single-character or multi-character roleplay |
| Command | Description |
|---|---|
/character |
Show the active character |
/character list |
List the world's cast |
/character <name> |
Switch the active character |
/character set <field> <value> |
Edit a field — name, role, backstory, skills, interests, avoid, … |
/character add <name> |
Add a new character and make them active |
| Command | Description |
|---|---|
/sessions |
Interactive session picker |
/session <id-or-name> |
Switch to a session by ID prefix or name |
/session |
Show current session detail |
/new [name] |
Create a new session with current world settings |
| Command | Description |
|---|---|
/model |
Interactive model picker (lists all providers) |
/model <name> |
Switch to a specific model directly |
/models |
Alias for /model |
/pull <name> |
Pull a model from Ollama or HuggingFace |
| Command | Description |
|---|---|
/memory |
Show session facts, summary, world state, stances |
/memory global |
Show this character's cross-session global memory |
/recap |
Recap what carried over into this world from earlier sessions |
/forget <text> |
Remove matching facts from session |
/search <query> |
Full-text search over session messages |
/pin |
Pin the last assistant message (keeps it in context) |
/unpin |
Unpin the last pinned message |
| Command | Description |
|---|---|
/notes |
View session notes |
/notes set <text> |
Write session notes |
/notes clear |
Clear session notes |
/settings |
Show key settings |
/settings <key.path> |
Inspect a specific setting |
/set <key.path> <value> |
Change a setting (e.g. /set general.temperature 0.9) |
| Command | Description |
|---|---|
/key set anthropic <key> |
Store Anthropic API key (encrypted) |
/key set openai <key> |
Store OpenAI API key (encrypted) |
/key set gemini <key> |
Store Gemini API key (encrypted) |
/key set nomi <key> |
Store Nomi.ai API key (encrypted) |
/key set kindroid <key> |
Store Kindroid.ai API key (encrypted) |
/key set cloudflare <accountId>:<apiToken> |
Store Cloudflare credentials (encrypted) |
/key set bedrock <accessKeyId>:<secretAccessKey>:<region> |
Store AWS Bedrock credentials (encrypted) |
/key list |
Show configured providers |
/key remove <provider> |
Delete a key |
Keys can also be set via .env — see Environment below. The encrypted DB key takes priority over the env var if both are present.
| Command | Description |
|---|---|
/status |
Show full status — world, session, model, memory stats |
/export |
Export session as markdown to data/exports/ |
/export <filename> |
Export with a specific filename |
/help |
List all commands |
/exit |
Exit |
you › Hello
elara › I'm Elara, keeper of the Rusty Flagon...
──────────────────────────────────────────────────
› your message here
──────────────────────────────────────────────────
[oread] Fantasy Tavern • llama3.2 • session
Role labels align to a fixed column. Status bar sits below the input — world, model, session update live after every command.
Model names are routed automatically by prefix:
claude-* → Anthropic (ANTHROPIC_API_KEY or /key set anthropic)
gpt-*, o1-*, o3-* → OpenAI (OPENAI_API_KEY or /key set openai)
gemini-* → Gemini (GEMINI_API_KEY or /key set gemini)
nomi-* → Nomi.ai (NOMI_API_KEY or /key set nomi)
kindroid-* → Kindroid.ai (KINDROID_API_KEY or /key set kindroid)
@cf/* → Cloudflare Workers AI (CF_ACCOUNT_ID + CF_API_TOKEN or /key set cloudflare)
bedrock:* → AWS Bedrock (AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION or /key set bedrock)
anything else → Ollama (local, no key needed)
Bedrock model IDs carry a bedrock: prefix and use the unified Converse API, so Claude, Llama, Nova, and Mistral all stream through one path — e.g. bedrock:anthropic.claude-3-5-sonnet-20241022-v2:0 or, for cross-region inference profiles, bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0. The region and the model's access entitlement come from your AWS credentials.
Switch mid-conversation with /model <name> — world, memory, and session stay the same.
/pull accepts Ollama model names, HuggingFace repo paths, and full HuggingFace resolve URLs:
/pull llama3.2
/pull hf.co/bartowski/Llama-3.2-3B-Instruct-GGUF
/pull https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_K_M.ggufA progress bar replaces the input while downloading. ESC cancels. On completion the model is automatically set as active.
A world is a self-contained roleplay setting: its lore and opening scene, the narrator's voice, hard rules the characters must obey, and a cast of one or more characters (name, role, backstory, traits). 19 built-in worlds span romance, mystery, sci-fi, fantasy, and slice-of-life companions.
data/templates/
defaults/ # 19 built-in worlds (read-only)
user/ # Your saved worlds — created via /world save
active.json # Currently active settings
Build your own without touching a file: load a world, reshape it live with /world set, /world rule, and /character set|add, then /world save to fork it into a reusable user world. You can also drop any compatible JSON straight into data/templates/user/, or set CHAT_TEMPLATES_DIR=/path/to/other/defaults to load worlds from another directory.
oread keeps a long roleplay coherent without you managing context by hand. After each turn, a small local extraction model (phi4-mini via Ollama) updates a layered memory in the background — it never blocks the reply:
- World state — current location and in-story time, who's physically present, ongoing events, recent discoveries, and mood, with a lifecycle so stale events fade out.
- Rolling summary — older turns are condensed so the thread survives well past the context window.
- Character stances & debates — positions the character holds and open disagreements, so they don't silently flip-flop.
- Global memory (per character) — durable facts promoted across sessions, scoped to the character that earned them so a sci-fi captain never recalls your vampire romance.
- Relationships — trust and familiarity that evolve with each session's tenor; the character is told whether you're newly met, close, or estranged.
- Carryover — when you return to a world, the newest snapshot seeds the new session and the prompt opens with "Earlier in this world…".
/recapshows it;/memoryinspects the rest.
Everything is assembled into the prompt within a token budget, newest and most relevant first. Toggle the background work with /set general.autoSummarize and /set general.crossSessionMemory.
When running with --api, the server exposes:
GET /api/health
GET /api/models
POST /api/models/pull SSE download progress
POST /api/chat SSE streaming { message, sessionId? }
GET/POST /api/sessions
GET/PUT/DELETE /api/sessions/:id
GET /api/sessions/:id/messages
PATCH /api/sessions/:id/messages/:messageId/pin
GET/PUT /api/sessions/:id/notes
GET /api/sessions/:id/world-state
GET /api/sessions/:id/search?q=
GET/PUT/DELETE /api/templates
GET/PUT/DELETE /api/templates/active
POST /api/templates/user
GET/PUT/DELETE /api/memory/global
GET /api/memory/search?q=
GET /api/memory/relationships
- API keys encrypted with AES-256-GCM; secret auto-generated at
data/.secret(chmod 600) - Set
OREAD_SECRETenv var to use your own passphrase - API server binds to
127.0.0.1only — not reachable from the network without a reverse proxy - Export filenames are sanitized to prevent path traversal
- LLM responses are sanitized via
responseGuard: strips ANSI/terminal escape sequences and flags prompt-injection patterns before display or persistence - FTS5 queries are hardened against injection; UUID validation on all session API routes
settings_snapshotis stripped from all API responses- No telemetry; no outbound connections except Ollama (local) and configured cloud providers
OLLAMA_URL=http://localhost:11434
OLLAMA_CHAT_MODEL=llama3.2
OLLAMA_EXTRACTION_MODEL=phi4-mini
OLLAMA_MAX_LOADED_MODELS=2 # Keep chat + extraction models warm simultaneously
CHAT_TEMPLATES_DIR= # Optional: load worlds from another directory
API_PORT=3002
OREAD_SECRET= # Optional: custom encryption passphrase
# Cloud provider API keys (alternative to /key set <provider> <key>)
ANTHROPIC_API_KEY=
OPENAI_API_KEY=
GEMINI_API_KEY=
NOMI_API_KEY=
KINDROID_API_KEY=
# Cloudflare Workers AI (both required as env var alternative)
CF_ACCOUNT_ID= # Cloudflare account ID
CF_API_TOKEN= # Cloudflare API token (Workers AI: Run permission)
# AWS Bedrock (all three required as env var alternative)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION= # e.g. us-east-1 — must have Bedrock model access enabled
AWS_SESSION_TOKEN= # Optional: only for temporary STS credentials
# Character/model IDs for companion providers
NOMI_MODEL= # Nomi companion UUID
KINDROID_MODEL= # Kindroid AI IDCopy .env.example to .env to configure.
npm run build # Compile JSX + bundle to dist/oread.js
npm run watch # Rebuild on file changes
npm run dev # Build + runAfter any source change: npm run build then oread.