Skip to content

A character engine for conversational AI that runs as a CLI application

Notifications You must be signed in to change notification settings

rakazirut/understudy

Repository files navigation

understudy

A character engine for conversational AI that runs as a CLI application. Drop a system prompt into prompts/ and the engine handles the rest -- persistent memory across sessions, optional ElevenLabs voice output, Obsidian vault integration for grounding characters in personal knowledge, and multi-character group conversations. No characters are included; bring your own.

Prerequisites

  • Claude CLI installed and authenticated (claude available on PATH)
  • Node.js 22+
  • ElevenLabs account (optional -- required only for voice output in single-character mode)

Setup

git clone <repo-url> && cd understudy
npm install
cp .env.example .env
# Edit .env with your values (see below)
npm run dev

Environment Variables

Variable Required Description
CHARACTER_NAME No Skip the selection menu. Comma-separated for multi-character: CHARACTER_NAME=Ghost,Hanni.
UNDERSTUDY_MODEL No Claude model for conversations. Defaults to sonnet.
UNDERSTUDY_EXTRACT_MODEL No Claude model for memory extraction/summarization. Defaults to haiku.
UNDERSTUDY_COLOR_{NAME} No Terminal color for a character's name. Values: purple, blue, cyan, green, yellow, red.
ELEVENLABS_API_KEY No ElevenLabs API key. Voice output is disabled without this.
ELEVENLABS_VOICE_ID No Fallback ElevenLabs voice ID. Per-character override: ELEVENLABS_VOICE_ID_{NAME}.
ELEVENLABS_MODEL_ID No ElevenLabs TTS model. Defaults to eleven_turbo_v2_5.
OBSIDIAN_VAULT_PATH No Absolute path to an Obsidian vault directory. Enables vault RAG when set.
UNDERSTUDY_LOG_DIR No Directory for markdown chat log transcripts. Logs are written on session end when set.

Voice requires ELEVENLABS_API_KEY and a voice ID for the active character (either ELEVENLABS_VOICE_ID_{NAME} or the fallback ELEVENLABS_VOICE_ID). Voice is only available in single-character mode.

Architecture

src/
  index.ts              Conversation loop, session management, shutdown/memory extraction
  claude.ts             Claude CLI wrapper (spawns `claude` as a child process)
  types.ts              Shared types: Message, CharacterSession, TranscriptEntry
  log.ts                Markdown transcript writer (writes to UNDERSTUDY_LOG_DIR on session end)
  config/
    character.ts        CharacterConfig factory, terminal color resolution
    select.ts           Scans prompts/ for available characters, single and multi-select menus
  memory/
    db.ts               SQLite setup (better-sqlite3), migrations, WAL mode
    store.ts            Memory CRUD -- scoring by importance, recency, frequency
    extract.ts          Post-conversation memory extraction + summarization via Claude
    context.ts          Assembles memory context block for the system prompt
  prompt/
    system.ts           Reads character definition, appends memory context
  vault/
    config.ts           Vault path validation and budget constants
    scanner.ts          Recursive .md file walker with mtime diffing
    index.ts            FTS5 indexing -- chunking by headings, frontmatter/wikilink stripping
    search.ts           Per-message FTS5 search with stopword filtering and deduplication
  voice/
    tts.ts              ElevenLabs streaming TTS (single-character mode only)
    stt.ts              Placeholder -- not implemented
prompts/
  {name}-system.md      Character definition and behavioral rules
data/
  {name}.db             SQLite database per character (created automatically, gitignored)

How It Works

Single-Character Mode

On startup with one character selected:

  1. Character selection: if CHARACTER_NAME is set (single value), that character is used. If only one *-system.md exists in prompts/, it's auto-selected. If multiple exist, an interactive menu is shown.
  2. SQLite database initializes (creates data/{character}.db on first run)
  3. If OBSIDIAN_VAULT_PATH is set, vault markdown files are incrementally indexed into FTS5
  4. System prompt is assembled: character definition from prompts/{character}-system.md + relevant memories from past conversations

Each message:

  1. Vault is searched for relevant snippets (FTS5 match against extracted keywords)
  2. Matching vault snippets are prepended to the message as <vault-reference> blocks
  3. Message is sent to Claude CLI with the system prompt and session continuity (--session-id / --resume)
  4. Response is printed (and streamed through ElevenLabs TTS if voice is enabled)

On exit (bye, quit, exit, goodbye, /quit, /exit, or Ctrl+C):

  1. Claude extracts new memories from the conversation transcript
  2. Conversation summary and mood are stored
  3. Memories persist to SQLite for future sessions

Multi-Character Mode

Select multiple characters via comma-separated CHARACTER_NAME=Ghost,Hanni or by entering comma-separated numbers (or all) at the interactive menu.

Each character gets their own database, Claude session, and memory store. Characters are identified by colored names in the terminal (configurable via UNDERSTUDY_COLOR_{NAME}).

How messages are routed:

  • Unaddressed messages go to all characters sequentially. Each character sees what prior characters said before responding.
  • @Name message sends only to the named character.

Reaction rounds: After the addressed character(s) respond, every other character gets a chance to react to what was said. If a character has nothing to add, they respond with [PASS] and stay silent. Reactions continue for up to 3 rounds or until everyone passes. This means characters naturally respond to each other without the user having to relay messages.

On shutdown, each character independently extracts memories and summarizes the conversation. Characters remember what other characters said in group conversations.

Limitations in multi-character mode:

  • TTS is disabled
  • Vault is indexed per-character DB (redundant but simple)

Creating a New Character

  1. Create prompts/{name}-system.md with the character's system prompt and behavioral rules
  2. Restart the bot -- the new character appears in the selection menu
  3. Each character gets their own data/{name}.db with separate memories, conversations, and vault index

Vault Integration

Set OBSIDIAN_VAULT_PATH to the root of an Obsidian vault. On startup, understudy walks the vault directory, indexes all .md files into an FTS5 virtual table, and on subsequent runs only re-indexes files whose mtime has changed.

What gets indexed:

  • All .md files recursively (excluding .obsidian/, .trash/, dotfiles, .icloud stubs)
  • YAML frontmatter is stripped before indexing
  • Wikilinks are resolved to plain text
  • Files are chunked by headings (h1-h3), capped at ~2,000 chars per chunk

Per message, up to 4 snippets are retrieved (best-ranked per file), with a total budget of 6,400 characters. Snippets are injected as context the character "already knows" -- they draw on them naturally without referencing them as notes or files.

iCloud note: If your vault syncs via iCloud, files may appear as .icloud stubs when not downloaded locally. These are skipped during scanning. Make sure the files you want indexed are downloaded to disk.

Chat Logs

Set UNDERSTUDY_LOG_DIR to persist conversation transcripts as markdown files. On session end, a file is written with YAML frontmatter and speaker-labeled messages:

---
date: 2026-02-20
characters: [Ghost, Hanni]
---

**You:** @Ghost what did you think about chapter 12?

**Ghost:** She doesn't break...

**Hanni:** Ghost just said that...

Filename format: {YYYY-MM-DD}-{HH-MM}-{character names}.md. The directory is created automatically if it doesn't exist. If UNDERSTUDY_LOG_DIR is unset, no logs are written.

Docker: When UNDERSTUDY_LOG_DIR is set in .env, it's mounted into the container at /app/logs. The host path receives the transcript files directly.

Scripts

Command Description
npm run dev Run with tsx (development)
npm run build Compile TypeScript to dist/
npm start Run compiled output

About

A character engine for conversational AI that runs as a CLI application

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors