Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
65 KiB
JSON
1 line
65 KiB
JSON
{"repo": "arkascendedai/tracyhill-rp", "files": {"README.md": {"9008232ff20db711ee922b1edca8a5268e89cab0": {"t": 1785747283.2738304, "n": 28928, "h": "abe7eedac7f13518", "c": "# TracyHill RP\n\n**A self-hosted, multi-user LLM chat client built for long-form collaborative fiction and roleplaying.**\n\nTracyHill RP is what you get when you stop fighting your chat client and start building one around the way long-form roleplay actually works: persistent campaigns, structured world state, version-controlled system prompts, multi-provider model switching mid-conversation, and all the admin surface you need to run it for a small group of friends or a private community.\n\nEvery API call is proxied server-side so provider keys never touch the browser. SQLite-backed, Docker-deployable, MIT licensed, and genuinely designed to be cloned and run — the default `docker compose up` produces a working instance with a demo admin account you can log into immediately.\n\n---\n\n## Feature Tour\n\n### Multi-provider chat\nEvery major frontier provider, with native support for each one's differences instead of flattening them to a lowest common denominator.\n\n| Provider | Models | Highlights |\n|---|---|---|\n| **Anthropic** | Claude Fable 5 (always-on adaptive thinking), Opus 4.8 / 4.7 (adaptive-only thinking), Opus 4.6 (1M ctx, 128K out), Sonnet 4.6, Haiku 4.5 | Off / Budget / Adaptive thinking, effort control (Low → Max), prompt caching with per-session TTL, fast-mode toggle, categorized refusal cards, serving-model transparency |\n| **OpenAI** | GPT-5.5 / 5.5 Pro, GPT-5.4 / 5.4 Pro / Mini / Nano, GPT-5.1 / Codex-Mini, GPT-5 / Mini / Nano, GPT-4.1 / Mini, GPT Chat (Instant) | Reasoning models via the Responses API with visible thinking summaries (effort None → XHigh per model), Chat Completions for non-reasoning, cached-input accounting |\n| **Google** | Gemini 3.1 Pro, Gemini 3.5 Flash, Gemini 3.1 Flash-Lite, Gemini 2.5 Pro / Flash / Flash-Lite | Thinking levels on 3.x, thinking budget on 2.5 (true off where the API permits, honest always-on where it doesn't), thinking-token accounting, long-context tier pricing in the cost overlay, native PDF + image support |\n| **xAI** | Grok 4.3, Grok 4.20 (R/NR) | 1M context, streamed reasoning summaries, reasoning-effort control incl. true non-reasoning, cached-input pricing, long-context tier pricing |\n| **DeepSeek** | DeepSeek V4 Pro / Flash | On/off thinking toggle with streamed reasoning content, cache-hit pricing, 1M context / 384K output |\n| **z.ai** | GLM-5.1, GLM-5, GLM-5 Turbo, GLM-4.7, GLM-4.7 FlashX, GLM-4.6, GLM-4.5 | On/off thinking toggle with streamed reasoning content, cache-hit pricing |\n| **Xiaomi** | MiMo v2.5 Pro, MiMo v2.5 | 1M context, on/off thinking toggle with streamed reasoning content, cached-input pricing |\n| **Claude Code bridge** (optional) | Claude Fable 5 / Opus 4.8 / 4.7 / 4.6 / Sonnet 4.6 / Haiku 4.5 — `-bridge` model variants | Route chat through a [Claude Code SDK](https://docs.claude.com/en/docs/claude-code/overview) bridge that you host yourself, so chat usage bills against your Claude subscription instead of per-token Anthropic API |\n| **Custom endpoints** | OpenRouter, LM Studio, Ollama, Together AI, Groq, vLLM, anything OpenAI-compatible | Multiple named endpoints, per-endpoint API keys, custom model lists with configurable context/output limits, Chat Completions or Responses API formats |\n\nSwitch models mid-conversation with a single click. The custom dropdown groups models by provider with expandable submenus.\n\n### Campaign pipeline\nTracyHill RP treats long-form roleplay as a **stateful document workflow**, not an ephemeral chat. Every campaign has:\n- A **system prompt** (the persistent identity of the session)\n- A **lorebook** (a structured, queryable knowledge base — characters, locations, factions, events, world rules)\n- **Version history** — every system prompt change is archived in perpetuity\n\nA queue-driven worker runs the post-session update flow automatically:\n\n- **Rolling diff** — after each session-turn batch crosses a character threshold, a small model writes incremental lorebook edits (create / update / disable entries) directly from the transcript.\n- **Repetition detection** — periodically scans for narrative repetition and proposes anti-repetition rules (ban / limit / vary tiers) that get injected into the system prompt.\n- **Sysprompt audit** — periodically reviews the system prompt for drift against the live lorebook state and rewrites it surgically.\n- **Lorebook consolidation** — periodically dedupes and merges related entries.\n- **Lorebook archival** — entries that haven't been activated in many turns get compressed into a synopsis \"trigger\" that's still searchable but no longer carries narrative cost. Their full content stays in cold storage and gets lazily inflated back into context when the trigger activates.\n- **Narrative thread tracking** — a worker maintains a live index of open story threads (status, last progress, next beat) as an always-in-context lorebook entry, with a full detail entry per thread retrieved on demand. Resolved threads fall off through a grace window and graduate into the event history, so the index stays focused on what's still live.\n\nPer-campaign serialization with mutual exclusion means jobs queue cleanly; the UI shows a running/queued pill with elapsed timers. Pipeline state persists to disk so a worker restart doesn't lose progress.\n\n### Context engine\n\nEvery turn assembles its system-prompt context from the lorebook through a multi-signal retrieval pipeline rather than a fixed-shape document. Per turn:\n\n- **Keyword activation** — entries with matching trigger keys are pulled in, with configurable scan depth and sticky/cooldown behavior.\n- **Semantic activation** — embedding cosine similarity against the user's turn (OpenAI `text-embedding-3-large` / `-3-small` or Google `gemini-embedding-2`, configurable per campaign).\n- **HyDE query expansion** — a small model rewrites the user's turn into a hypothetical-answer query to widen semantic recall.\n- **Synonym key expansion** — when an entry is created or updated by the rolling-diff worker, its key list is automatically widened with synonym variations so it activates on alternate phrasings.\n- **Researcher pass** — a small LLM picks contextually relevant entries the keyword + semantic passes missed.\n- **Scene presence override** — characters physically present in the current scene always get their firmware loaded regardless of activation scores.\n- **Budget pruning** — entries scored and fitted into a configurable token budget (16K default). Guaranteed entries (constant, sticky, scene-present) always make it; the rest compete on score.\n\n**Scene markers** — assistant responses emit `[SCENE]` blocks tagging location, present characters, present-but-unaware characters, in-world date/time, and notes. The parser strips the block from visible output and persists it as structured data. Downstream turns get a `<scene_state>` context injection enforcing knowledge boundaries across scene transitions, plus per-character **epistemic scoping** via `known_by` tags on lorebook entries (Scene Knowledge vs Narrator-Only Knowledge sections in the prompt).\n\n**Scene presence validator** — a small model post-checks each assistant turn for present-character drift and surfaces a three-way UI resolution (accept the validator's pick, accept the model's pick, or regenerate the response). The same pass reconciles per-character **attire state**, surfaced as a `<character_attire>` context block on subsequent turns with freshness annotation so wardrobe continuity survives long scenes.\n\n**Anti-repetition rules** — tiered `ban` / `limit` / `vary` rules injected into the chat system prompt to push back against narrative loops. Auto-dedup, archival, and budget cap.\n\nA per-turn **Context Preview panel** in the chat surface shows exactly what was activated, what scored highest, and what got dropped for budget — so prompt-engineering becomes legible instead of a black box.\n\n### Campaign wizard\nStarting a new campaign from scratch? The wizard is an LLM-guided interactive conversation that gathers your premise, main character, NPCs, world, and rules, then produces **four documents** (state seed v0, system prompt, seed update template, system-prompt update template) plus a brand new session pre-loaded with them. The wizard is multi-model — you pick which provider drives it.\n\n### Chat power tools\n- **Markdown rendering** with code blocks, dialogue highlighting, copy buttons\n- **File attachments** — text, PDFs, images (base64 for chat providers that support vision)\n- **Image generation** — GPT Image 2, Gemini 3.1 Flash Image, Grok Imagine, GLM Image — all at max native resolution\n- **Multi-session streaming** — stream multiple sessions concurrently, sidebar shows live dots\n- **Browser-disconnect recovery** — the server accumulates streams independently and saves the result even if you close the tab mid-response\n- **Output-truncation detection** — visible warning if a response hit `max_tokens`\n- **Serving-model transparency** — a badge whenever the provider reports a response was produced by a different model than the one requested (provider-side substitutions and fallbacks are never silent)\n- **Full token accounting** — input / output / reasoning / cache read / cache write tokens per message, with per-model pricing (including long-context tiers and cache-hit rates) rolled into a live session cost estimate\n- **Background-task observability** — embedding, retrieval, validator, and pipeline failures are recorded as system events and surfaced in the UI instead of failing silently\n- **Concurrent message editing** — edit, delete, resend, regenerate, cut-after, copy\n- **Search** — global search across all sessions plus in-session `Ctrl+F`\n- **Session organization** — nested folders with drag-and-drop, recycle bin with soft delete + auto-purge\n- **Prompt templates** — reusable, inserted as attachment chips, not pasted text\n- **Per-user runtime defaults** — cache TTL, thinking mode, effort, temperature, auto-scroll, font size\n\n### Admin surface\n- **Users** — create / delete users, reset passwords, toggle admin role, view any user's sessions\n- **Provider keys** — set server-level fallback keys, see per-provider status, manage custom endpoints\n- **Storage** — disk usage stats, image count and size, bulk image purge\n- **Audit** — database-backed audit log of admin/auth/pipeline/wizard actions\n- **Claude Code bridge** — drive a remote Claude Code agent from an in-app dialog (optional, disabled by default)\n- **Codex bridge** — drive a remote OpenAI Codex CLI session from an in-app dialog (optional, disabled by default)\n\n### Security\n- Per-user session cookies (`httpOnly`, `secure`, `sameSite:lax`), signed with a rotation-safe secret\n- Server-side API proxy — provider keys never reach the browser\n- Provider keys encrypted at rest with AES-256-GCM, key derived from `SESSION_SECRET` via HKDF\n- Content-Security-Policy (`script-src 'self'`, no `unsafe-inline`, no `unsafe-eval`), HSTS, X-Frame-Options DENY, Referrer-Policy, Permissions-Policy\n- CSRF Origin + `X-Requested-With` custom-header validation on every state-changing request\n- Per-IP and per-username rate limiting on auth endpoints (login: 3 fails / 30 min lockout; MFA / forgot-password / registration: 10 / 15 min)\n- Password complexity enforcement, bcrypt cost-12 hashing, timing-safe code comparison\n- Session regeneration + explicit save on login (prevents session fixation, prevents new-session-ID loss on early disconnect)\n- Multi-factor authentication via email, trust-device cookies (30-day, SHA-256-hashed at rest), forgot-password flow with **constant-shape response** (no account enumeration)\n- IP allowlist (gates upstream TCP peer, not end users — right layer for reverse-proxy restriction)\n- **SSRF-gated custom endpoints** — custom `baseUrl` values must parse as `http(s)://` with no userinfo, resolve to a public IP (loopback / RFC1918 / link-local / CGNAT / reserved IPv4 + ::1 / fc00::/7 / fe80::/10 / multicast IPv6 all rejected). Operators can opt-in to specific LAN hostnames via `CUSTOM_ENDPOINT_ALLOW_HOSTS`. Upstream error bodies from custom endpoints are stripped so the SSE error event can't be used as a readback channel.\n- Bounded upstream error-body reads (16 KB cap) prevent a misbehaving provider from pinning RAM\n- Upstream request timeouts prevent resource leaks from hung provider requests\n- Admin cannot delete or demote the last admin account\n- Cascade-clean account deletion (audit log retained on purpose; all per-user data + active HTTP sessions evicted)\n- Non-root container user (UID 1001), atomic file writes, sanitized error responses (never leak stack traces)\n\n### What you don't need to run it\n- **No external database.** SQLite with WAL journaling, one file under `./data/`.\n- **No Redis.** Sessions are stored in the same SQLite database — durable across restarts, no extra service.\n- **No Kafka.** The worker polls the DB directly for pipeline/wizard jobs.\n- **No S3.** Images live as flat files in `./data/v2/images/`.\n- **No separate auth service.** Everything is in-process.\n\n---\n\n## A note on content\n\nTracyHill RP is a general-purpose collaborative-fiction tool. The tone, content rating, and subject matter of any given campaign are defined entirely by the user via the wizard — anywhere from light-rated PG family-friendly adventure to adult literary fiction with mature themes. The application itself does not filter, rate, or moderate content; what the model writes is a function of (1) the system prompt and lorebook the user authored, (2) the configured provider's own usage policies, and (3) whatever moderation you choose to layer on top.\n\nThe wizard ships with a sample campaign template intended to illustrate the structural format (scene rules, character profiles, voice modes, voice firmware) rather than to set a tone — operators are expected to replace or adjust it to suit their audience. If you're running a public or multi-user instance, treat the wizard template, the chosen providers, and the user agreement as a single content-policy surface.\n\n---\n\n## Quick Start\n\nYou need Docker with the `docker compose` plugin. That's it.\n\n```bash\n# 1. Clone\ngit clone https://github.com/ArkAscendedAI/tracyhill-rp.git\ncd tracyhill-rp\n\n# 2. Copy the example env (SESSION_SECRET auto-generates on first boot if left blank)\ncp .env.example .env\n# Optionally pre-set a secret: echo \"SESSION_SECRET=$(openssl rand -hex 32)\" >> .env\n\n# 3. Build and start\ndocker compose up -d\n\n# 4. Open http://localhost:3000 and log in\n# Username: demo\n# Password: demo-pass\n```\n\nThat's the full setup. You're logged in as a demo admin. Go to **Settings → Provider Keys** to add API keys for whichever providers you want to use, and start a session.\n\n> **Change the demo password before exposing this to the internet.** The default credentials are public knowledge — that's the whole point of \"works out of the box.\" Go to **Settings → Change Password** as soon as you're in, or set `SEED_DEMO_USER=0` in `.env` and create users manually.\n\n### Verify it's healthy\n\n```bash\ncurl http://localhost:3000/api/system/health\n# {\"status\":\"ok\",\"db\":\"ok\",\"images\":\"ok\"}\n```\n\n### Tear it down\n\n```bash\ndocker compose down # stops containers, keeps data\ndocker compose down -v # stops and also wipes volumes (but not ./data which is a bind mount)\nrm -rf ./data # wipes all users, sessions, images — total reset\n```\n\n---\n\n## Installation Options\n\n### Option A: Docker Compose (recommended)\n\nThe shipped `docker-compose.yml` builds a multi-stage image, starts the API container, starts the API and a dedicated worker container, bind-mounts `./data` for persistence, sets up log rotation, and wires a healthcheck on `/api/system/health`.\n\nSee [Quick Start](#quick-start). This is the intended path for almost every user.\n\n### Option B: Local development (npm)\n\nIf you want to hack on the code, run each workspace in dev mode with hot reload:\n\n```bash\n# Install dependencies (uses npm workspaces)\nnpm install\n\n# Run migrations once\n# (the API auto-migrates on first boot, but if you want to run explicitly:)\nnpm run --workspace packages/db migrate\n\n# In three terminals:\nnpm run dev:api # apps/api on port 4010 (or PORT env), auto-restart on changes\nnpm run dev:worker # apps/worker, same\nnpm run dev:web # apps/web on port 5173, Vite dev server with HMR\n```\n\nCopy `.env.example` to `.env` at the repo root and set `SESSION_SECRET`. The API reads it from there.\n\nOpen `http://localhost:5173` — the Vite dev server proxies `/api/*` to the API on 3000.\n\n### Option C: Build from source + run with Node\n\n```bash\nnpm install\nnpm run build # typechecks + builds every workspace\nnpm run start:api # runs apps/api/src/index.ts via node with tsx loader\n# in another terminal:\nnpm run start:worker # runs apps/worker/src/index.ts\n```\n\nExpects environment variables to be set in the shell (e.g. via `set -a; source .env; set +a`).\n\n---\n\n## Configuration\n\nA minimal working `.env` needs exactly one line:\n\n```bash\nSESSION_SECRET=your-64-hex-random-string-here\n```\n\nEverything else has sensible defaults. [`ENVIRONMENT.md`](ENVIRONMENT.md) documents every single variable — name, default, what it does, and when you'd change it. The quick reference:\n\n| Category | Key variables |\n|---|---|\n| **Core** | `SESSION_SECRET` (required), `PORT`, `HOST_PORT`, `DB_FILE`, `LOG_LEVEL`, `TRUST_PROXY`, `ALLOWED_IPS` |\n| **Providers** (all optional fallbacks) | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `ZAI_API_KEY`, `DEEPSEEK_API_KEY` |\n| **Email** (for MFA/registration) | `SENDGRID_API_KEY`, `EMAIL_FROM`, `EMAIL_FROM_NAME` |\n| **Bootstrap** | `SEED_DEMO_USER`, `DEMO_USERNAME`, `DEMO_PASSWORD` |\n| **Mocking** | `MOCK_PROVIDER`, `EXPOSE_AUTH_CODES` |\n\nSee [`ENVIRONMENT.md`](ENVIRONMENT.md) for the full reference.\n\n---\n\n## Adding Provider Keys\n\nThere are two ways to configure API keys:\n\n### Per-user (recommended)\n\nLog in as an admin. Open **Settings → Provider Keys**. Paste your key for each provider you want to use. Keys are stored per-user in the database and are never exposed to the browser — the frontend only sees a redacted preview and status.\n\nThis is the right approach if:\n- You want different users to use their own accounts with each provider\n- You don't want to restart the server to rotate a key\n- You don't want to put keys in environment variables\n\n### Server fallback (env)\n\nSet `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. in `.env`. These become **fallback keys** — used when a user hasn't configured their own. User-supplied keys always override the env-level fallback.\n\nThis is useful if:\n- You're running a single-user instance and don't want to click through the UI\n- You want to share a team API budget without each user managing their own keys\n- You're running an internal tool with a shared billing account\n\nBoth approaches can coexist.\n\n### Custom OpenAI-compatible endpoints\n\nAny provider that speaks OpenAI's API format (Chat Completions or Responses) can be added at runtime via **Settings → Provider Keys → Custom Endpoints**:\n\n- **Name** (display label, e.g. \"Local LM Studio\")\n- **Base URL** (e.g. `http://192.168.1.10:1234/v1`)\n- **API Key** (leave blank for local/no-auth servers)\n- **API Format** (`chat-completions` or `responses`)\n- **Auth Header** (`Bearer`, `api-key`, or `none`)\n- **Models** — define which models your endpoint serves, each with its context and output limits\n\nCustom endpoints participate in browser-disconnect recovery, streaming, and per-message model switching just like built-in providers.\n\n---\n\n## Architecture\n\nTracyHill RP is a TypeScript monorepo using npm workspaces, with each concern isolated in its own package or app:\n\n```\napps/\n api/ Express HTTP API. Auth, streaming proxy, workspace CRUD,\n admin routes, pipeline/wizard orchestration, SPA serving.\n web/ React 18 + Vite frontend. Session UI, composer, dialogs,\n streaming rendering, markdown, drag-drop, admin panels.\n worker/ Background job runner. Polls SQLite for pipeline + wizard\n jobs, executes LLM calls, persists results.\n\npackages/\n contracts/ Zod schemas shared between frontend and backend.\n Any API request/response body is defined here.\n db/ SQLite schema (Drizzle ORM), migrations, client\n factory. Better-sqlite3 with WAL journaling.\n logging/ Structured pino logger with child-logger pattern,\n request-ID middleware, audit event helpers.\n model-catalog/ The per-provider model registry with capabilities,\n context windows, output limits, thinking/reasoning\n metadata. Single source of truth for what works.\n provider-runtime/ Provider dispatch. Abstracts each provider's wire\n format (Anthropic, OpenAI Responses/Chat Completions,\n Gemini, xAI, z.ai, DeepSeek, custom endpoints) behind\n a normalized stream contract.\n test-fixtures/ Deterministic test data used by every workspace's\n test suite.\n\ntools/\n codex-agent-service/ Optional OpenAI Codex bridge agent service.\n\ndocs/v2/ Per-slice engineering documentation. Each slice doc\n describes a vertical feature: its goals, DB changes, API\n surface, frontend changes, and tests.\n```\n\nThe API and worker share the same SQLite database. Frontend streams from the API; the API proxies to providers or enqueues worker jobs. The worker updates the DB as jobs progress and the API's polling endpoints surface that state to the frontend without needing websockets.\n\n### Why SQLite\n\nBecause for a self-hosted, single-host, small-scale service this is boring and correct. Better-sqlite3 + WAL gives you:\n- Atomic writes without a separate transaction log daemon\n- Concurrent reads with single-writer semantics (fine for chat workloads)\n- Backup with `cp` or `sqlite3 .backup`\n- No network hop between API and DB\n- One file to move, copy, replicate, or restore\n\nIf you outgrow SQLite, the DB layer is isolated in `packages/db` behind Drizzle — swapping for Postgres is a mechanical change, not an architectural one.\n\n### Data model at a glance\n\n```\nusers — account + role, password hash, email, MFA state,\n trusted devices, agreement timestamps\nuser_preferences — per-user sidebar state, active session, runtime defaults\nfolders — nested session folders, drag-drop ordering\nsessions — session metadata (name, model, runtime config,\n folder/campaign linkage, soft-delete, scene state)\nmessages — append-only message log per session\nmessage_attachments — text/image/PDF attachments referenced by messages\ncampaigns — campaign records with system prompt, context defaults,\n character roster, version counter\ncampaign_versions — archived system prompt per approved pipeline run\nlorebook_entries — structured world-knowledge entries activated per turn\nlorebook_entry_embeddings — per-entry vectors for semantic retrieval\nlorebook_activation_state — per-session activation tracking (sticky, cooldown,\n last-activated turn)\npipeline_runs — pipeline execution state (persisted across restarts)\npipeline_run_artifacts — raw LLM I/O capture for the pipeline workers\nwizard_runs — wizard execution state + generated documents\nwizard_templates — per-user wizard example templates (4 tabs)\ncustom_endpoints — OpenAI-compatible custom provider records\nprovider_keys — encrypted per-user API keys per provider\naudit_events — database-backed audit log of admin/auth/mutation actions\ngenerated_images — metadata for images generated via the image-gen APIs\nprompt_templates — reusable prompt template library per user\npending_assistant_messages — in-flight assistant messages awaiting completion\nhttp_sessions — SQLite-backed session store (durable across restarts)\n```\n\nFull schema lives in `packages/db/src/schema/`.\n\n---\n\n## Development\n\n### Running tests\n\nEvery workspace ships vitest suites. Run all tests from the repo root:\n\n```bash\nnpm test # runs vitest across every workspace\nnpm run typecheck # TypeScript typecheck across every workspace\nnpm run build # builds every workspace (tsc + vite)\n```\n\nPer-workspace runs are also supported:\n\n```bash\nnpm test --workspace apps/api\nnpm test --workspace packages/provider-runtime\n```\n\nEnd-to-end browser tests live in `apps/web/e2e/`:\n\n```bash\nnpm run test:e2e\n```\n\n### Adding a new provider\n\n1. Add the wire implementation to `packages/provider-runtime/src/<provider>.ts` following the existing pattern (`anthropic.ts`, `openai.ts`, etc.). Each provider exports a `callModel()` function and a stream parser.\n2. Register the provider in `packages/provider-runtime/src/registry.ts`.\n3. Add the model catalog entries to `packages/model-catalog/src/<provider>.ts` with `maxOut`, `ctx`, reasoning/thinking flags, and any other per-model capabilities.\n4. Add the provider enum to `packages/contracts/src/provider.ts`.\n5. Add the env var to `.env.example` and `ENVIRONMENT.md`.\n6. Write tests in `packages/provider-runtime/src/<provider>.test.ts` using the test fixtures.\n\n### Database migrations\n\nMigrations live in `packages/db/migrations/` and are auto-applied at API startup. To add a new one:\n\n```bash\ncd packages/db\nnpm run generate # generates a new migration from schema changes\nnpm run migrate # applies pending migrations\n```\n\n### Style notes\n\n- Strict TypeScript everywhere. `any` needs a comment explaining why.\n- Contracts are Zod schemas, typed via `z.infer<>`, never hand-written interfaces.\n- Every API route has a controller, a schema, and a test.\n- Every feature has a per-slice doc under `docs/v2/`.\n\n---\n\n## Deployment\n\n### Behind a reverse proxy\n\nThe shipped Compose file listens on `0.0.0.0:3000`. Production deployments should:\n\n1. Put nginx / Caddy / Traefik in front and terminate TLS there.\n2. Set `TRUST_PROXY=true` in `.env` so the API honors `X-Forwarded-For`.\n3. Consider `ALLOWED_IPS=<your-proxy-ip>` to reject direct connections that bypass the proxy.\n4. Tighten `HOST_PORT` to `127.0.0.1:3000` in Compose so only the proxy can reach the API.\n5. Disable `SEED_DEMO_USER` (`SEED_DEMO_USER=0`) and create accounts manually via the admin panel.\n\n### Data persistence\n\nThe shipped Compose file bind-mounts `./data` to `/app/data` inside the container. Everything stateful lives there:\n\n```\n./data/\n v2/\n tracyhill-rp-v2.sqlite # main database\n tracyhill-rp-v2.sqlite-wal # WAL journal (needed for consistent backup)\n tracyhill-rp-v2.sqlite-shm # shared memory file\n images/ # generated + uploaded images\n```\n\nTo back up, take a consistent SQLite snapshot and tar the images dir:\n\n```bash\nsqlite3 ./data/v2/tracyhill-rp-v2.sqlite \".backup ./data/backup.sqlite\"\ntar czf tracyhill-rp-backup-$(date +%F).tar.gz \\\n ./data/backup.sqlite ./data/v2/images\nrm ./data/backup.sqlite\n```\n\nDon't just `cp` the .sqlite file while the app is running — that can produce a corrupt copy due to WAL. Always use `.backup` or stop the container first.\n\n### Upgrading\n\n```bash\ngit pull\ndocker compose build\ndocker compose up -d\n```\n\nMigrations run automatically at API startup. `./data` persists across restarts.\n\n### Updating the demo password after first boot\n\n1. Log in as `demo` / `demo-pass`\n2. Go to **Settings → Change Password**\n3. Set a strong password\n4. (Optional) disable auto-seeding: edit `.env`, set `SEED_DEMO_USER=0`, `docker compose up -d` to recreate the container\n\n---\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, branch etiquette, and how to propose features.\n\nThe short version: fork, hack, write tests, open a PR. Every change should land with at least one test, a lint-clean build, and a typecheck-clean tree.\n\n---\n\n## License\n\nMIT. See [`LICENSE`](LICENSE).\n\nTracyHill RP is a personal project released as open source. It ships as-is with no warranty. If you run it, you're running it on your own infrastructure, under your own keys, for your own users — the maintainer makes no guarantees about uptime, security, or fitness for any particular purpose. Read the LICENSE before deploying to anything you care about.\n\n---\n\n## Acknowledgments\n\nTracyHill RP was built for a specific use case (long-form collaborative roleplay with a small group) and released open source in the hope that it's useful to anyone else running into the same frustrations with off-the-shelf chat clients. If you find it useful, a star on GitHub is appreciated. If you find a bug, an issue or a PR is even better.\n"}, "b5e935f0f874d8647ad6d2c3122bf700fc299351": {"t": 1785772138.429046, "n": 28928, "h": "abe7eedac7f13518", "c": "# TracyHill RP\n\n**A self-hosted, multi-user LLM chat client built for long-form collaborative fiction and roleplaying.**\n\nTracyHill RP is what you get when you stop fighting your chat client and start building one around the way long-form roleplay actually works: persistent campaigns, structured world state, version-controlled system prompts, multi-provider model switching mid-conversation, and all the admin surface you need to run it for a small group of friends or a private community.\n\nEvery API call is proxied server-side so provider keys never touch the browser. SQLite-backed, Docker-deployable, MIT licensed, and genuinely designed to be cloned and run — the default `docker compose up` produces a working instance with a demo admin account you can log into immediately.\n\n---\n\n## Feature Tour\n\n### Multi-provider chat\nEvery major frontier provider, with native support for each one's differences instead of flattening them to a lowest common denominator.\n\n| Provider | Models | Highlights |\n|---|---|---|\n| **Anthropic** | Claude Fable 5 (always-on adaptive thinking), Opus 4.8 / 4.7 (adaptive-only thinking), Opus 4.6 (1M ctx, 128K out), Sonnet 4.6, Haiku 4.5 | Off / Budget / Adaptive thinking, effort control (Low → Max), prompt caching with per-session TTL, fast-mode toggle, categorized refusal cards, serving-model transparency |\n| **OpenAI** | GPT-5.5 / 5.5 Pro, GPT-5.4 / 5.4 Pro / Mini / Nano, GPT-5.1 / Codex-Mini, GPT-5 / Mini / Nano, GPT-4.1 / Mini, GPT Chat (Instant) | Reasoning models via the Responses API with visible thinking summaries (effort None → XHigh per model), Chat Completions for non-reasoning, cached-input accounting |\n| **Google** | Gemini 3.1 Pro, Gemini 3.5 Flash, Gemini 3.1 Flash-Lite, Gemini 2.5 Pro / Flash / Flash-Lite | Thinking levels on 3.x, thinking budget on 2.5 (true off where the API permits, honest always-on where it doesn't), thinking-token accounting, long-context tier pricing in the cost overlay, native PDF + image support |\n| **xAI** | Grok 4.3, Grok 4.20 (R/NR) | 1M context, streamed reasoning summaries, reasoning-effort control incl. true non-reasoning, cached-input pricing, long-context tier pricing |\n| **DeepSeek** | DeepSeek V4 Pro / Flash | On/off thinking toggle with streamed reasoning content, cache-hit pricing, 1M context / 384K output |\n| **z.ai** | GLM-5.1, GLM-5, GLM-5 Turbo, GLM-4.7, GLM-4.7 FlashX, GLM-4.6, GLM-4.5 | On/off thinking toggle with streamed reasoning content, cache-hit pricing |\n| **Xiaomi** | MiMo v2.5 Pro, MiMo v2.5 | 1M context, on/off thinking toggle with streamed reasoning content, cached-input pricing |\n| **Claude Code bridge** (optional) | Claude Fable 5 / Opus 4.8 / 4.7 / 4.6 / Sonnet 4.6 / Haiku 4.5 — `-bridge` model variants | Route chat through a [Claude Code SDK](https://docs.claude.com/en/docs/claude-code/overview) bridge that you host yourself, so chat usage bills against your Claude subscription instead of per-token Anthropic API |\n| **Custom endpoints** | OpenRouter, LM Studio, Ollama, Together AI, Groq, vLLM, anything OpenAI-compatible | Multiple named endpoints, per-endpoint API keys, custom model lists with configurable context/output limits, Chat Completions or Responses API formats |\n\nSwitch models mid-conversation with a single click. The custom dropdown groups models by provider with expandable submenus.\n\n### Campaign pipeline\nTracyHill RP treats long-form roleplay as a **stateful document workflow**, not an ephemeral chat. Every campaign has:\n- A **system prompt** (the persistent identity of the session)\n- A **lorebook** (a structured, queryable knowledge base — characters, locations, factions, events, world rules)\n- **Version history** — every system prompt change is archived in perpetuity\n\nA queue-driven worker runs the post-session update flow automatically:\n\n- **Rolling diff** — after each session-turn batch crosses a character threshold, a small model writes incremental lorebook edits (create / update / disable entries) directly from the transcript.\n- **Repetition detection** — periodically scans for narrative repetition and proposes anti-repetition rules (ban / limit / vary tiers) that get injected into the system prompt.\n- **Sysprompt audit** — periodically reviews the system prompt for drift against the live lorebook state and rewrites it surgically.\n- **Lorebook consolidation** — periodically dedupes and merges related entries.\n- **Lorebook archival** — entries that haven't been activated in many turns get compressed into a synopsis \"trigger\" that's still searchable but no longer carries narrative cost. Their full content stays in cold storage and gets lazily inflated back into context when the trigger activates.\n- **Narrative thread tracking** — a worker maintains a live index of open story threads (status, last progress, next beat) as an always-in-context lorebook entry, with a full detail entry per thread retrieved on demand. Resolved threads fall off through a grace window and graduate into the event history, so the index stays focused on what's still live.\n\nPer-campaign serialization with mutual exclusion means jobs queue cleanly; the UI shows a running/queued pill with elapsed timers. Pipeline state persists to disk so a worker restart doesn't lose progress.\n\n### Context engine\n\nEvery turn assembles its system-prompt context from the lorebook through a multi-signal retrieval pipeline rather than a fixed-shape document. Per turn:\n\n- **Keyword activation** — entries with matching trigger keys are pulled in, with configurable scan depth and sticky/cooldown behavior.\n- **Semantic activation** — embedding cosine similarity against the user's turn (OpenAI `text-embedding-3-large` / `-3-small` or Google `gemini-embedding-2`, configurable per campaign).\n- **HyDE query expansion** — a small model rewrites the user's turn into a hypothetical-answer query to widen semantic recall.\n- **Synonym key expansion** — when an entry is created or updated by the rolling-diff worker, its key list is automatically widened with synonym variations so it activates on alternate phrasings.\n- **Researcher pass** — a small LLM picks contextually relevant entries the keyword + semantic passes missed.\n- **Scene presence override** — characters physically present in the current scene always get their firmware loaded regardless of activation scores.\n- **Budget pruning** — entries scored and fitted into a configurable token budget (16K default). Guaranteed entries (constant, sticky, scene-present) always make it; the rest compete on score.\n\n**Scene markers** — assistant responses emit `[SCENE]` blocks tagging location, present characters, present-but-unaware characters, in-world date/time, and notes. The parser strips the block from visible output and persists it as structured data. Downstream turns get a `<scene_state>` context injection enforcing knowledge boundaries across scene transitions, plus per-character **epistemic scoping** via `known_by` tags on lorebook entries (Scene Knowledge vs Narrator-Only Knowledge sections in the prompt).\n\n**Scene presence validator** — a small model post-checks each assistant turn for present-character drift and surfaces a three-way UI resolution (accept the validator's pick, accept the model's pick, or regenerate the response). The same pass reconciles per-character **attire state**, surfaced as a `<character_attire>` context block on subsequent turns with freshness annotation so wardrobe continuity survives long scenes.\n\n**Anti-repetition rules** — tiered `ban` / `limit` / `vary` rules injected into the chat system prompt to push back against narrative loops. Auto-dedup, archival, and budget cap.\n\nA per-turn **Context Preview panel** in the chat surface shows exactly what was activated, what scored highest, and what got dropped for budget — so prompt-engineering becomes legible instead of a black box.\n\n### Campaign wizard\nStarting a new campaign from scratch? The wizard is an LLM-guided interactive conversation that gathers your premise, main character, NPCs, world, and rules, then produces **four documents** (state seed v0, system prompt, seed update template, system-prompt update template) plus a brand new session pre-loaded with them. The wizard is multi-model — you pick which provider drives it.\n\n### Chat power tools\n- **Markdown rendering** with code blocks, dialogue highlighting, copy buttons\n- **File attachments** — text, PDFs, images (base64 for chat providers that support vision)\n- **Image generation** — GPT Image 2, Gemini 3.1 Flash Image, Grok Imagine, GLM Image — all at max native resolution\n- **Multi-session streaming** — stream multiple sessions concurrently, sidebar shows live dots\n- **Browser-disconnect recovery** — the server accumulates streams independently and saves the result even if you close the tab mid-response\n- **Output-truncation detection** — visible warning if a response hit `max_tokens`\n- **Serving-model transparency** — a badge whenever the provider reports a response was produced by a different model than the one requested (provider-side substitutions and fallbacks are never silent)\n- **Full token accounting** — input / output / reasoning / cache read / cache write tokens per message, with per-model pricing (including long-context tiers and cache-hit rates) rolled into a live session cost estimate\n- **Background-task observability** — embedding, retrieval, validator, and pipeline failures are recorded as system events and surfaced in the UI instead of failing silently\n- **Concurrent message editing** — edit, delete, resend, regenerate, cut-after, copy\n- **Search** — global search across all sessions plus in-session `Ctrl+F`\n- **Session organization** — nested folders with drag-and-drop, recycle bin with soft delete + auto-purge\n- **Prompt templates** — reusable, inserted as attachment chips, not pasted text\n- **Per-user runtime defaults** — cache TTL, thinking mode, effort, temperature, auto-scroll, font size\n\n### Admin surface\n- **Users** — create / delete users, reset passwords, toggle admin role, view any user's sessions\n- **Provider keys** — set server-level fallback keys, see per-provider status, manage custom endpoints\n- **Storage** — disk usage stats, image count and size, bulk image purge\n- **Audit** — database-backed audit log of admin/auth/pipeline/wizard actions\n- **Claude Code bridge** — drive a remote Claude Code agent from an in-app dialog (optional, disabled by default)\n- **Codex bridge** — drive a remote OpenAI Codex CLI session from an in-app dialog (optional, disabled by default)\n\n### Security\n- Per-user session cookies (`httpOnly`, `secure`, `sameSite:lax`), signed with a rotation-safe secret\n- Server-side API proxy — provider keys never reach the browser\n- Provider keys encrypted at rest with AES-256-GCM, key derived from `SESSION_SECRET` via HKDF\n- Content-Security-Policy (`script-src 'self'`, no `unsafe-inline`, no `unsafe-eval`), HSTS, X-Frame-Options DENY, Referrer-Policy, Permissions-Policy\n- CSRF Origin + `X-Requested-With` custom-header validation on every state-changing request\n- Per-IP and per-username rate limiting on auth endpoints (login: 3 fails / 30 min lockout; MFA / forgot-password / registration: 10 / 15 min)\n- Password complexity enforcement, bcrypt cost-12 hashing, timing-safe code comparison\n- Session regeneration + explicit save on login (prevents session fixation, prevents new-session-ID loss on early disconnect)\n- Multi-factor authentication via email, trust-device cookies (30-day, SHA-256-hashed at rest), forgot-password flow with **constant-shape response** (no account enumeration)\n- IP allowlist (gates upstream TCP peer, not end users — right layer for reverse-proxy restriction)\n- **SSRF-gated custom endpoints** — custom `baseUrl` values must parse as `http(s)://` with no userinfo, resolve to a public IP (loopback / RFC1918 / link-local / CGNAT / reserved IPv4 + ::1 / fc00::/7 / fe80::/10 / multicast IPv6 all rejected). Operators can opt-in to specific LAN hostnames via `CUSTOM_ENDPOINT_ALLOW_HOSTS`. Upstream error bodies from custom endpoints are stripped so the SSE error event can't be used as a readback channel.\n- Bounded upstream error-body reads (16 KB cap) prevent a misbehaving provider from pinning RAM\n- Upstream request timeouts prevent resource leaks from hung provider requests\n- Admin cannot delete or demote the last admin account\n- Cascade-clean account deletion (audit log retained on purpose; all per-user data + active HTTP sessions evicted)\n- Non-root container user (UID 1001), atomic file writes, sanitized error responses (never leak stack traces)\n\n### What you don't need to run it\n- **No external database.** SQLite with WAL journaling, one file under `./data/`.\n- **No Redis.** Sessions are stored in the same SQLite database — durable across restarts, no extra service.\n- **No Kafka.** The worker polls the DB directly for pipeline/wizard jobs.\n- **No S3.** Images live as flat files in `./data/v2/images/`.\n- **No separate auth service.** Everything is in-process.\n\n---\n\n## A note on content\n\nTracyHill RP is a general-purpose collaborative-fiction tool. The tone, content rating, and subject matter of any given campaign are defined entirely by the user via the wizard — anywhere from light-rated PG family-friendly adventure to adult literary fiction with mature themes. The application itself does not filter, rate, or moderate content; what the model writes is a function of (1) the system prompt and lorebook the user authored, (2) the configured provider's own usage policies, and (3) whatever moderation you choose to layer on top.\n\nThe wizard ships with a sample campaign template intended to illustrate the structural format (scene rules, character profiles, voice modes, voice firmware) rather than to set a tone — operators are expected to replace or adjust it to suit their audience. If you're running a public or multi-user instance, treat the wizard template, the chosen providers, and the user agreement as a single content-policy surface.\n\n---\n\n## Quick Start\n\nYou need Docker with the `docker compose` plugin. That's it.\n\n```bash\n# 1. Clone\ngit clone https://github.com/ArkAscendedAI/tracyhill-rp.git\ncd tracyhill-rp\n\n# 2. Copy the example env (SESSION_SECRET auto-generates on first boot if left blank)\ncp .env.example .env\n# Optionally pre-set a secret: echo \"SESSION_SECRET=$(openssl rand -hex 32)\" >> .env\n\n# 3. Build and start\ndocker compose up -d\n\n# 4. Open http://localhost:3000 and log in\n# Username: demo\n# Password: demo-pass\n```\n\nThat's the full setup. You're logged in as a demo admin. Go to **Settings → Provider Keys** to add API keys for whichever providers you want to use, and start a session.\n\n> **Change the demo password before exposing this to the internet.** The default credentials are public knowledge — that's the whole point of \"works out of the box.\" Go to **Settings → Change Password** as soon as you're in, or set `SEED_DEMO_USER=0` in `.env` and create users manually.\n\n### Verify it's healthy\n\n```bash\ncurl http://localhost:3000/api/system/health\n# {\"status\":\"ok\",\"db\":\"ok\",\"images\":\"ok\"}\n```\n\n### Tear it down\n\n```bash\ndocker compose down # stops containers, keeps data\ndocker compose down -v # stops and also wipes volumes (but not ./data which is a bind mount)\nrm -rf ./data # wipes all users, sessions, images — total reset\n```\n\n---\n\n## Installation Options\n\n### Option A: Docker Compose (recommended)\n\nThe shipped `docker-compose.yml` builds a multi-stage image, starts the API container, starts the API and a dedicated worker container, bind-mounts `./data` for persistence, sets up log rotation, and wires a healthcheck on `/api/system/health`.\n\nSee [Quick Start](#quick-start). This is the intended path for almost every user.\n\n### Option B: Local development (npm)\n\nIf you want to hack on the code, run each workspace in dev mode with hot reload:\n\n```bash\n# Install dependencies (uses npm workspaces)\nnpm install\n\n# Run migrations once\n# (the API auto-migrates on first boot, but if you want to run explicitly:)\nnpm run --workspace packages/db migrate\n\n# In three terminals:\nnpm run dev:api # apps/api on port 4010 (or PORT env), auto-restart on changes\nnpm run dev:worker # apps/worker, same\nnpm run dev:web # apps/web on port 5173, Vite dev server with HMR\n```\n\nCopy `.env.example` to `.env` at the repo root and set `SESSION_SECRET`. The API reads it from there.\n\nOpen `http://localhost:5173` — the Vite dev server proxies `/api/*` to the API on 3000.\n\n### Option C: Build from source + run with Node\n\n```bash\nnpm install\nnpm run build # typechecks + builds every workspace\nnpm run start:api # runs apps/api/src/index.ts via node with tsx loader\n# in another terminal:\nnpm run start:worker # runs apps/worker/src/index.ts\n```\n\nExpects environment variables to be set in the shell (e.g. via `set -a; source .env; set +a`).\n\n---\n\n## Configuration\n\nA minimal working `.env` needs exactly one line:\n\n```bash\nSESSION_SECRET=your-64-hex-random-string-here\n```\n\nEverything else has sensible defaults. [`ENVIRONMENT.md`](ENVIRONMENT.md) documents every single variable — name, default, what it does, and when you'd change it. The quick reference:\n\n| Category | Key variables |\n|---|---|\n| **Core** | `SESSION_SECRET` (required), `PORT`, `HOST_PORT`, `DB_FILE`, `LOG_LEVEL`, `TRUST_PROXY`, `ALLOWED_IPS` |\n| **Providers** (all optional fallbacks) | `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`, `XAI_API_KEY`, `ZAI_API_KEY`, `DEEPSEEK_API_KEY` |\n| **Email** (for MFA/registration) | `SENDGRID_API_KEY`, `EMAIL_FROM`, `EMAIL_FROM_NAME` |\n| **Bootstrap** | `SEED_DEMO_USER`, `DEMO_USERNAME`, `DEMO_PASSWORD` |\n| **Mocking** | `MOCK_PROVIDER`, `EXPOSE_AUTH_CODES` |\n\nSee [`ENVIRONMENT.md`](ENVIRONMENT.md) for the full reference.\n\n---\n\n## Adding Provider Keys\n\nThere are two ways to configure API keys:\n\n### Per-user (recommended)\n\nLog in as an admin. Open **Settings → Provider Keys**. Paste your key for each provider you want to use. Keys are stored per-user in the database and are never exposed to the browser — the frontend only sees a redacted preview and status.\n\nThis is the right approach if:\n- You want different users to use their own accounts with each provider\n- You don't want to restart the server to rotate a key\n- You don't want to put keys in environment variables\n\n### Server fallback (env)\n\nSet `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. in `.env`. These become **fallback keys** — used when a user hasn't configured their own. User-supplied keys always override the env-level fallback.\n\nThis is useful if:\n- You're running a single-user instance and don't want to click through the UI\n- You want to share a team API budget without each user managing their own keys\n- You're running an internal tool with a shared billing account\n\nBoth approaches can coexist.\n\n### Custom OpenAI-compatible endpoints\n\nAny provider that speaks OpenAI's API format (Chat Completions or Responses) can be added at runtime via **Settings → Provider Keys → Custom Endpoints**:\n\n- **Name** (display label, e.g. \"Local LM Studio\")\n- **Base URL** (e.g. `http://192.168.1.10:1234/v1`)\n- **API Key** (leave blank for local/no-auth servers)\n- **API Format** (`chat-completions` or `responses`)\n- **Auth Header** (`Bearer`, `api-key`, or `none`)\n- **Models** — define which models your endpoint serves, each with its context and output limits\n\nCustom endpoints participate in browser-disconnect recovery, streaming, and per-message model switching just like built-in providers.\n\n---\n\n## Architecture\n\nTracyHill RP is a TypeScript monorepo using npm workspaces, with each concern isolated in its own package or app:\n\n```\napps/\n api/ Express HTTP API. Auth, streaming proxy, workspace CRUD,\n admin routes, pipeline/wizard orchestration, SPA serving.\n web/ React 18 + Vite frontend. Session UI, composer, dialogs,\n streaming rendering, markdown, drag-drop, admin panels.\n worker/ Background job runner. Polls SQLite for pipeline + wizard\n jobs, executes LLM calls, persists results.\n\npackages/\n contracts/ Zod schemas shared between frontend and backend.\n Any API request/response body is defined here.\n db/ SQLite schema (Drizzle ORM), migrations, client\n factory. Better-sqlite3 with WAL journaling.\n logging/ Structured pino logger with child-logger pattern,\n request-ID middleware, audit event helpers.\n model-catalog/ The per-provider model registry with capabilities,\n context windows, output limits, thinking/reasoning\n metadata. Single source of truth for what works.\n provider-runtime/ Provider dispatch. Abstracts each provider's wire\n format (Anthropic, OpenAI Responses/Chat Completions,\n Gemini, xAI, z.ai, DeepSeek, custom endpoints) behind\n a normalized stream contract.\n test-fixtures/ Deterministic test data used by every workspace's\n test suite.\n\ntools/\n codex-agent-service/ Optional OpenAI Codex bridge agent service.\n\ndocs/v2/ Per-slice engineering documentation. Each slice doc\n describes a vertical feature: its goals, DB changes, API\n surface, frontend changes, and tests.\n```\n\nThe API and worker share the same SQLite database. Frontend streams from the API; the API proxies to providers or enqueues worker jobs. The worker updates the DB as jobs progress and the API's polling endpoints surface that state to the frontend without needing websockets.\n\n### Why SQLite\n\nBecause for a self-hosted, single-host, small-scale service this is boring and correct. Better-sqlite3 + WAL gives you:\n- Atomic writes without a separate transaction log daemon\n- Concurrent reads with single-writer semantics (fine for chat workloads)\n- Backup with `cp` or `sqlite3 .backup`\n- No network hop between API and DB\n- One file to move, copy, replicate, or restore\n\nIf you outgrow SQLite, the DB layer is isolated in `packages/db` behind Drizzle — swapping for Postgres is a mechanical change, not an architectural one.\n\n### Data model at a glance\n\n```\nusers — account + role, password hash, email, MFA state,\n trusted devices, agreement timestamps\nuser_preferences — per-user sidebar state, active session, runtime defaults\nfolders — nested session folders, drag-drop ordering\nsessions — session metadata (name, model, runtime config,\n folder/campaign linkage, soft-delete, scene state)\nmessages — append-only message log per session\nmessage_attachments — text/image/PDF attachments referenced by messages\ncampaigns — campaign records with system prompt, context defaults,\n character roster, version counter\ncampaign_versions — archived system prompt per approved pipeline run\nlorebook_entries — structured world-knowledge entries activated per turn\nlorebook_entry_embeddings — per-entry vectors for semantic retrieval\nlorebook_activation_state — per-session activation tracking (sticky, cooldown,\n last-activated turn)\npipeline_runs — pipeline execution state (persisted across restarts)\npipeline_run_artifacts — raw LLM I/O capture for the pipeline workers\nwizard_runs — wizard execution state + generated documents\nwizard_templates — per-user wizard example templates (4 tabs)\ncustom_endpoints — OpenAI-compatible custom provider records\nprovider_keys — encrypted per-user API keys per provider\naudit_events — database-backed audit log of admin/auth/mutation actions\ngenerated_images — metadata for images generated via the image-gen APIs\nprompt_templates — reusable prompt template library per user\npending_assistant_messages — in-flight assistant messages awaiting completion\nhttp_sessions — SQLite-backed session store (durable across restarts)\n```\n\nFull schema lives in `packages/db/src/schema/`.\n\n---\n\n## Development\n\n### Running tests\n\nEvery workspace ships vitest suites. Run all tests from the repo root:\n\n```bash\nnpm test # runs vitest across every workspace\nnpm run typecheck # TypeScript typecheck across every workspace\nnpm run build # builds every workspace (tsc + vite)\n```\n\nPer-workspace runs are also supported:\n\n```bash\nnpm test --workspace apps/api\nnpm test --workspace packages/provider-runtime\n```\n\nEnd-to-end browser tests live in `apps/web/e2e/`:\n\n```bash\nnpm run test:e2e\n```\n\n### Adding a new provider\n\n1. Add the wire implementation to `packages/provider-runtime/src/<provider>.ts` following the existing pattern (`anthropic.ts`, `openai.ts`, etc.). Each provider exports a `callModel()` function and a stream parser.\n2. Register the provider in `packages/provider-runtime/src/registry.ts`.\n3. Add the model catalog entries to `packages/model-catalog/src/<provider>.ts` with `maxOut`, `ctx`, reasoning/thinking flags, and any other per-model capabilities.\n4. Add the provider enum to `packages/contracts/src/provider.ts`.\n5. Add the env var to `.env.example` and `ENVIRONMENT.md`.\n6. Write tests in `packages/provider-runtime/src/<provider>.test.ts` using the test fixtures.\n\n### Database migrations\n\nMigrations live in `packages/db/migrations/` and are auto-applied at API startup. To add a new one:\n\n```bash\ncd packages/db\nnpm run generate # generates a new migration from schema changes\nnpm run migrate # applies pending migrations\n```\n\n### Style notes\n\n- Strict TypeScript everywhere. `any` needs a comment explaining why.\n- Contracts are Zod schemas, typed via `z.infer<>`, never hand-written interfaces.\n- Every API route has a controller, a schema, and a test.\n- Every feature has a per-slice doc under `docs/v2/`.\n\n---\n\n## Deployment\n\n### Behind a reverse proxy\n\nThe shipped Compose file listens on `0.0.0.0:3000`. Production deployments should:\n\n1. Put nginx / Caddy / Traefik in front and terminate TLS there.\n2. Set `TRUST_PROXY=true` in `.env` so the API honors `X-Forwarded-For`.\n3. Consider `ALLOWED_IPS=<your-proxy-ip>` to reject direct connections that bypass the proxy.\n4. Tighten `HOST_PORT` to `127.0.0.1:3000` in Compose so only the proxy can reach the API.\n5. Disable `SEED_DEMO_USER` (`SEED_DEMO_USER=0`) and create accounts manually via the admin panel.\n\n### Data persistence\n\nThe shipped Compose file bind-mounts `./data` to `/app/data` inside the container. Everything stateful lives there:\n\n```\n./data/\n v2/\n tracyhill-rp-v2.sqlite # main database\n tracyhill-rp-v2.sqlite-wal # WAL journal (needed for consistent backup)\n tracyhill-rp-v2.sqlite-shm # shared memory file\n images/ # generated + uploaded images\n```\n\nTo back up, take a consistent SQLite snapshot and tar the images dir:\n\n```bash\nsqlite3 ./data/v2/tracyhill-rp-v2.sqlite \".backup ./data/backup.sqlite\"\ntar czf tracyhill-rp-backup-$(date +%F).tar.gz \\\n ./data/backup.sqlite ./data/v2/images\nrm ./data/backup.sqlite\n```\n\nDon't just `cp` the .sqlite file while the app is running — that can produce a corrupt copy due to WAL. Always use `.backup` or stop the container first.\n\n### Upgrading\n\n```bash\ngit pull\ndocker compose build\ndocker compose up -d\n```\n\nMigrations run automatically at API startup. `./data` persists across restarts.\n\n### Updating the demo password after first boot\n\n1. Log in as `demo` / `demo-pass`\n2. Go to **Settings → Change Password**\n3. Set a strong password\n4. (Optional) disable auto-seeding: edit `.env`, set `SEED_DEMO_USER=0`, `docker compose up -d` to recreate the container\n\n---\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, branch etiquette, and how to propose features.\n\nThe short version: fork, hack, write tests, open a PR. Every change should land with at least one test, a lint-clean build, and a typecheck-clean tree.\n\n---\n\n## License\n\nMIT. See [`LICENSE`](LICENSE).\n\nTracyHill RP is a personal project released as open source. It ships as-is with no warranty. If you run it, you're running it on your own infrastructure, under your own keys, for your own users — the maintainer makes no guarantees about uptime, security, or fitness for any particular purpose. Read the LICENSE before deploying to anything you care about.\n\n---\n\n## Acknowledgments\n\nTracyHill RP was built for a specific use case (long-form collaborative roleplay with a small group) and released open source in the hope that it's useful to anyone else running into the same frustrations with off-the-shelf chat clients. If you find it useful, a star on GitHub is appreciated. If you find a bug, an issue or a PR is even better.\n"}}, ".env.example": {"9008232ff20db711ee922b1edca8a5268e89cab0": {"t": 1786067296.5932329, "n": 6161, "h": "452d918e8f945a9a", "c": "# ═══════════════════════════════════════════════════════════════════════════\n# TracyHill RP — Environment Configuration\n# ═══════════════════════════════════════════════════════════════════════════\n#\n# QUICK START:\n# 1. Copy this file to .env: cp .env.example .env\n# 2. Set SESSION_SECRET (below) to a random string:\n# openssl rand -hex 32\n# 3. docker compose up -d\n# 4. Open http://localhost:3000 and log in with: demo / demo-pass\n#\n# For a detailed explanation of every variable, see ENVIRONMENT.md.\n# ═══════════════════════════════════════════════════════════════════════════\n\n# ── Core Runtime ────────────────────────────────────────────────────────────\n\n# REQUIRED. Secret used to sign session cookies. Must be set before the app\n# will start. Generate with: openssl rand -hex 32\n# Never commit a real value to version control.\nSESSION_SECRET=\n\n# Port the API listens on inside the container.\nPORT=3000\n\n# Port exposed on the Docker host. Change if 3000 is in use.\nHOST_PORT=3000\n\n# SQLite database file path inside the container. Persisted via the ./data\n# bind mount in docker-compose.yml.\nDB_FILE=/app/data/v2/tracyhill-rp-v2.sqlite\n\n# Directory for generated/uploaded images, also persisted under ./data.\nIMAGE_DIR=/app/data/v2/images\n\n# Path to the built web app served by the API. Leave as default unless you're\n# customizing the build output location.\nWEB_DIST_DIR=/app/apps/web/dist\n\n# Run background workers (pipeline, wizard) inside the API process instead of\n# a separate container. Set to 1 for small deployments; leave 0 to use the\n# dedicated worker service in docker-compose.yml.\nINLINE_WORKERS=0\n\n# Interval in milliseconds for the worker to poll the pipeline queue.\nPIPELINE_POLL_MS=1000\n\n# Set to true when the API sits behind a reverse proxy (nginx, Caddy,\n# Traefik, Cloudflare) so it correctly reads X-Forwarded-For headers.\nTRUST_PROXY=false\n\n# IP allowlist for API access. \"*\" allows any source. Comma-separate specific\n# IPs or CIDRs for lockdown. This gates the upstream TCP peer, not end-user\n# IPs, so it's the right place to restrict which reverse-proxy hosts can\n# reach the API.\nALLOWED_IPS=*\n\n# Comma-separated allowlist of hostnames that bypass the SSRF private-IP\n# gate when users configure a Custom Endpoint baseUrl. By default every\n# custom-endpoint baseUrl must resolve to a public IP; set this to opt-in\n# specific LAN hosts (e.g. a local LM Studio or Ollama). Leave blank to\n# block all private/loopback custom endpoints.\n# CUSTOM_ENDPOINT_ALLOW_HOSTS=lmstudio.local,ollama.lan\n\n# Structured log verbosity. One of: trace | debug | info | warn | error\nLOG_LEVEL=info\n\n# Docker log rotation (json-file driver). See the `logging` section in\n# docker-compose.yml.\nDOCKER_LOG_MAX_SIZE=10m\nDOCKER_LOG_MAX_FILE=5\n\n# ── Chat Providers ──────────────────────────────────────────────────────────\n# All provider keys are optional server-level fallbacks. Users can override\n# any of them from the in-app Settings → Provider Keys dialog (their key is\n# stored per-user in the database and takes precedence over the env value).\n#\n# Leave every key blank to run in \"bring your own key\" mode — users must\n# enter their own key on first use. Fill one in here to skip that step for\n# everyone on the instance.\n\n# ANTHROPIC_API_KEY=\n# DEEPSEEK_API_KEY=\n# GOOGLE_API_KEY=\n# OPENAI_API_KEY=\n# XAI_API_KEY=\n# XIAOMI_API_KEY=\n# ZAI_API_KEY=\n\n# ── Claude Code Chat-Provider Bridge (optional) ─────────────────────────────\n# Enables a synthetic \"claude-code\" chat provider whose -bridge model variants\n# (claude-opus-4-7-bridge, etc.) route through a self-hosted Claude Code SDK\n# instance instead of the Anthropic per-token API. If you have a Claude\n# subscription you'd rather use for chat, point these variables at a Claude\n# Code agent service you host. Leave blank to hide the bridge provider.\n# This is a DIFFERENT service from the admin Claude Code bridge below — they\n# can run side-by-side or independently.\n\n# CLAUDE_CODE_BRIDGE_URL=\n# CLAUDE_CODE_BRIDGE_SECRET=\n\n# ── Email (SendGrid) ────────────────────────────────────────────────────────\n# Required for registration verification, forgot-password codes, and MFA.\n# If left blank, registration/MFA/password-reset flows will fail. Single-user\n# installs can skip this by enabling SEED_DEMO_USER below and never using\n# the self-service flows.\n\n# SENDGRID_API_KEY=\nEMAIL_FROM=noreply@example.com\nEMAIL_FROM_NAME=TracyHill RP\n\n# ── Claude Code Admin Bridge (optional admin feature) ──────────────────────\n# Enables the in-app Claude Code admin DIALOG that lets admins drive a remote\n# Claude Code agent over HTTPS (shell access, file edits, automation).\n# Leave blank to disable the dialog. This is SEPARATE from the chat-provider\n# bridge above; both can run side-by-side or independently.\n# See https://claude.com/claude-code for the agent-side project.\n\n# CLAUDE_CODE_HOST=\n# CLAUDE_CODE_PORT=7700\n# CLAUDE_CODE_SECRET=\n# CLAUDE_CODE_CA_PATH=certs/claude-agent.pem\n# CLAUDE_CODE_SERVERNAME=claude-agent\n\n# ── OpenAI Codex Bridge (optional admin feature) ────────────────────────────\n# Enables the in-app Codex dialog that lets admins drive a remote OpenAI\n# Codex CLI session over HTTPS. Leave blank to disable.\n\n# CODEX_HOST=\n# CODEX_PORT=7701\n# CODEX_SECRET=\n# CODEX_CA_PATH=certs/codex-agent.pem\n# CODEX_SERVERNAME=codex-agent\n\n# ── Development / Out-of-Box Bootstrap ──────────────────────────────────────\n# When SEED_DEMO_USER=1, the API creates a default admin account on first\n# boot using DEMO_USERNAME / DEMO_PASSWORD. Great for local development and\n# first-time setup. TURN THIS OFF IN PRODUCTION before exposing the server\n# to the internet, or change the password immediately after first login.\n\nSEED_DEMO_USER=1\nDEMO_USERNAME=demo\nDEMO_PASSWORD=demo-pass\n\n# Use a deterministic in-memory provider instead of real LLM APIs. Useful\n# for running tests and developing UI without burning tokens.\nMOCK_PROVIDER=0\n\n# When 1, MFA/registration/forgot-password codes are included in the JSON\n# response instead of only emailed. DO NOT enable outside of local dev —\n# this leaks secrets over the wire.\nEXPOSE_AUTH_CODES=0\n"}}}} |