Skip to content

Repository files navigation

GAIM — GM AI Manager

A play-by-post tabletop RPG management platform where the GM runs the game and Claude AI voices the NPCs and inactive player characters. Designed for async play: characters respond on their own schedule, the session log persists between sessions, and you can pick up exactly where you left off.


Table of Contents

  1. What It Is
  2. How It Works
  3. Starting a New Campaign
  4. Feature Reference
  5. Tech Stack
  6. Architecture
  7. Local Setup
  8. Campaign Structure
  9. Environment Variables
  10. Running the App
  11. UI Walkthrough
  12. Dice Rolling
  13. AI Character System
  14. Adding Features
  15. Key Files Quick Reference
  16. Data Models
  17. API Reference
  18. Troubleshooting

What It Is

GAIM is a browser-based game table for play-by-post RPGs. The GM types messages into a chat interface; player characters (PCs) and NPCs respond via Claude AI according to their individual character sheets, voice archetypes, campaign rules, and the session history.

The core loop:

  1. GM sets a scene and sends a message.
  2. The AI reads character sheets, rules, handouts, and recent history, then generates in-character responses for each relevant participant — staggered with realistic delays so they don't all reply at once.
  3. Responses appear in the chat log as if the characters are playing in real time.
  4. The GM reacts, the scene continues.

Everything persists to disk. Come back a week later, open the browser, and the log, players, scenes, and collected handouts are all there.


How It Works

Browser (React)  ←──SSE stream──  Express backend  ←──→  Claude API
     │                                   │
     │── POST /api/session/message ──→   │── writes SESSION.json
     │                                   │── queues AI responses
     │← SSE: new message events ─────── │── reads character .md files
                                         │── injects rules + handouts
                                         │── calls Claude per character

The backend is the single source of truth. The frontend never writes to disk — it posts to the API, which mutates SESSION.json and broadcasts changes over a persistent Server-Sent Events connection. Every open browser tab stays in sync automatically.


Starting a New Campaign

Step 1 — Create the campaign folder

Click + New Game at the bottom of the left sidebar. Type a name — the modal shows a live preview of the slug as you type (e.g. typing Mörk Borg: The Dying World previews campaigns/mork-borg-the-dying-world). Hit Create Campaign.

The app scaffolds the full folder structure on disk and prefills MEMORY.md with a commented template. A success notice appears listing the next steps.

Step 2 — Write your rules

Open the new campaign folder in a text editor and add .md files to rules/. All files in that directory are auto-loaded and injected into every AI prompt. For a crunchy system, split by topic rather than dumping everything in one file:

rules/
├── core-rules.md    ← dice, skill checks, death
├── combat.md        ← initiative, attacks, damage, armor
└── classes.md       ← class abilities, starting gear

Keep them as concise reference summaries — the AI needs mechanics it can apply, not lore prose.

Step 3 — Edit MEMORY.md

MEMORY.md is injected into every AI prompt as always-on context. The file comes prefilled with a template; fill in what applies and delete what doesn't:

  • Tone & Atmosphere — how the world should feel
  • Setting — facts every character would know
  • House Rules — rulings that override or extend the base system
  • Recurring Factions & NPCs — persistent groups and named contacts
  • Session Notes — running notes to carry between sessions

Step 4 — Switch to the new campaign

Open .env in a text editor, set DEFAULT_CAMPAIGN=your-campaign-slug, and restart the server (npm run dev). The server boots into one campaign at a time; this is the only way to switch.

Step 5 — Session Zero

With the new campaign loaded:

  1. Click the + next to SCENES in the left sidebar and toggle Session Zero on.
  2. Add a seat per player — give each a name (e.g. "Tom"). Each seat is auto-assigned a unique Jungian archetype, shown as small text below the name. Click change to override it.
  3. Create the scene. A SESSION ZERO section appears in the left sidebar listing all seats.
  4. Run the conversation. The AI voices each seat using their assigned archetype and no character sheet — they engage as players at a table: asking questions about the rules and world, discussing what kind of character they want to play, and working through character creation together. Guide them as GM and let the conversation drive the choices.
  5. When a player settles on their character, create their sheet at campaigns/your-campaign/characters/slug.md in a text editor outside the app.
  6. Back in the app, an Assign character… dropdown appears on each unfilled seat in the SESSION ZERO sidebar section. Select the new file to link it — this promotes the seat to a full player record and carries the archetype over automatically.

Step 6 — First scene

Create a regular scene (no Session Zero toggle), add your PCs and any NPCs as participants, and start play.


Feature Reference

Campaign Management

  • Markdown-based content: characters, NPCs, rules, handouts, and scenes are all .md files you edit in any text editor.
  • MEMORY.md — a freeform context document injected into every AI prompt. Use it for house rules, campaign tone, recurring themes, or anything the AI should always know.
  • Handout watching: drop a new file into handouts/ while the server is running and it becomes available instantly (no restart needed).
  • SESSION.json persists the full game state between server restarts.

Player Management

  • Add players by selecting any character from the campaign roster.
  • Track status per player: active, inactive, or dead.
  • Each player is automatically assigned one of 12 Jungian archetypes that shapes their AI voice (see AI Character System).
  • Poke button forces a character to respond right now, bypassing the normal timing delay.

Session Zero

Session Zero is a special scene mode for collaborative character creation before the campaign begins (or when a new player joins mid-campaign to replace a fallen character).

How it works:

  1. Click the + next to SCENES in the left sidebar and toggle Session Zero on.
  2. Add a seat for each player joining — give each seat a name (e.g. "Tom", "Dick", "Harry"). Each seat is auto-assigned a unique archetype, shown as small text below the name field. Click change to override it.
  3. Create the scene. A SESSION ZERO section appears in the left sidebar showing all seats.
  4. Run the conversation. The AI voices each seat using their assigned archetype — no character sheet needed. They engage as players at a table: asking questions, discussing character concepts, working through char-gen together. Guide them as GM.
  5. When a player has decided on their character, create their character .md file in campaigns/your-campaign/characters/ (outside the app, in any text editor).
  6. Back in the app, use the Assign character… dropdown on their seat to link the new character file to that seat. This adds the player to the roster and automatically inherits the seat's archetype — no re-assignment needed.

When a character dies mid-campaign:

The player (and their seat) are gone. A new player joins with a fresh mini-SZ: click + next to SCENES, enable the Session Zero toggle, and add a single seat for the new player. Run the conversation, create the character file, claim the seat. The new player gets their own archetype reflecting their own personality.

Key points:

  • Seats exist only for the SZ conversation — once a character is assigned, the seat is fulfilled.
  • The archetype on the seat transfers directly to the player record, so the AI's voice in the SZ carries seamlessly into the campaign.
  • A SZ scene behaves like any other scene for messaging and the chat log.

Scene Management

  • Create named scenes with a participant list (PCs + NPCs).
  • Switch the active scene to filter the chat log to just that scene's messages.
  • Add NPCs to the current scene mid-game by @-tagging them in the GM input.
  • Messages from before scenes existed still appear (legacy support).

Chat and Messages

  • GM messages are clearly labeled. PC/NPC messages show the character's name and avatar.
  • Edit or delete any message after the fact.
  • Pin messages to keep them in the AI context window even as the log grows long (important clues, key decisions, etc.).
  • Out-of-character text: wrap anything in (parentheses) and it renders with a distinct OOC style.
  • @-mentions auto-complete from the current cast and render as styled tags.
  • Markdown in messages: **bold**, _italic_, ~~strikethrough~~, code blocks, etc.

Dice Rolling

Type /r EXPRESSION in the GM input to roll dice. Results appear as a formatted message in the log. Full syntax in Dice Rolling.

AI Responses

  • Characters respond automatically after GM messages.
  • Two-round response system: initial replies first, then targeted follow-ups to whoever called them out.
  • Characters won't respond if they weren't addressed and nobody expects them to chime in.
  • Full prompt context includes: character sheet, voice archetype, campaign memory, game rules, scene NPCs, collected handouts, pinned messages, and the last 60 messages.
  • Characters can roll dice natively during responses (via Claude tool use).
  • Prompt caching enabled to keep token costs low.

Tech Stack

Layer Technology
Frontend React 18, Vite 5
Markdown rendering marked 18
Backend Node.js (ES modules), Express 4
AI Anthropic Claude SDK, claude-sonnet-4-6 default
Realtime Server-Sent Events (SSE)
File watching chokidar
Data storage JSON + Markdown files (no database)
Dev tooling concurrently, dotenv, node --watch

No database. No auth layer. Designed to run locally or on a private server.


Architecture

gaim/
├── backend/
│   ├── index.js              ← Express app setup, SSE endpoint, middleware
│   ├── start.js              ← Loads .env before anything else
│   ├── routes/
│   │   ├── campaign.js       ← Campaign data + tag list endpoints
│   │   ├── session.js        ← Messages, scenes, pins, handouts
│   │   └── agent.js          ← Manual poke endpoint
│   └── lib/
│       ├── agentRunner.js    ← Builds prompts, calls Claude, posts responses
│       ├── campaign.js       ← Reads campaign directory, watches handouts
│       ├── archetypes.js     ← 12 Jungian voice templates
│       ├── session.js        ← SESSION.json read/write with write-chain
│       └── eventBus.js       ← SSE broadcaster (all connected clients)
├── frontend/
│   ├── src/
│   │   ├── App.jsx           ← All state, SSE listener, top-level callbacks
│   │   ├── components/
│   │   │   ├── LeftSidebar.jsx   ← Campaign info, players, handouts, scenes
│   │   │   ├── ChatLog.jsx       ← Message list, scene filter
│   │   │   ├── Message.jsx       ← Individual message + edit/delete/pin
│   │   │   ├── GMInput.jsx       ← Text input, dice, @-tag autocomplete
│   │   │   ├── RightSidebar.jsx  ← Character sheets for active players
│   │   │   ├── SceneCreator.jsx  ← Scene creation modal
│   │   │   └── Markdown.jsx      ← Sanitized markdown renderer
│   │   └── *.css
│   └── vite.config.js        ← Proxies /api → localhost:3001
├── shared/
│   └── dice.js               ← Dice notation parser used by both sides
├── campaigns/
│   └── my-campaign/          ← Campaign data (edit freely in any text editor)
├── campaigns.config.json     ← Campaign registry: slugs, display names, last active
├── .env                      ← Secret keys and local config (never commit)
└── package.json              ← Root dev script (runs both servers)

State flow

All mutable state lives in SESSION.json on disk. The backend reads it fresh before every write to avoid conflicts. The frontend holds a copy in React state and rebuilds it from SSE events — it never reads the file directly.

SSE events broadcast: message, messageEdited, messageDeleted, messagePinned, playerAdded, playerRemoved, playerStatusChanged, sceneCreated, sceneActivated, participantAdded, handoutCollected.


Local Setup

Prerequisites

Install

# From the repo root
npm run install:all

This installs dependencies in root, backend/, and frontend/.

Configure

Create .env in the repo root (see Environment Variables for all options):

ANTHROPIC_API_KEY=sk-ant-...
DEFAULT_CAMPAIGN=my-campaign

Create a campaign

See Campaign Structure. A minimal campaign needs:

campaigns/my-campaign/
├── SESSION.json
├── characters/
│   └── hero.md
└── npcs/
    └── innkeeper.md

Minimal SESSION.json:

{
  "messages": [],
  "players": [],
  "scenes": [],
  "activeSceneId": null,
  "collectedHandouts": [],
  "pinnedMessageIds": []
}

Register the campaign in campaigns.config.json:

{
  "campaigns": ["my-campaign"],
  "lastActive": "my-campaign",
  "names": {
    "my-campaign": "My Campaign"
  }
}

The names map controls the display name shown in the sidebar header. If a campaign was created via + New Game, the name you typed is stored here automatically. If you created a campaign folder manually, you must add an entry to names — the fallback derives a title from the slug by splitting on hyphens and title-casing each word, which is lossy: diacritics, punctuation, and mixed case are not preserved (mork-borg-rotblack-sludge becomes Mork Borg Rotblack Sludge, not Mörk Borg: Rotblack Sludge).

Run

npm run dev

Opens:

Vite proxies all /api requests to the backend, so you only ever open the frontend URL.


Campaign Structure

Everything the AI knows comes from markdown files in the campaign directory. Edit them in any editor while the server is running — character and NPC changes take effect on the next agent call.

campaigns/my-campaign/
├── SESSION.json          ← Game state (auto-managed, don't hand-edit during play)
├── MEMORY.md             ← Always-on campaign context for the AI
├── characters/           ← Player character sheets
│   └── elara-swift.md
├── npcs/                 ← Non-player character sheets
│   └── lord-vareth.md
├── rules/                ← Game system rules, tables, mechanics
│   └── core-rules.md
├── handouts/             ← Documents/evidence players can collect
│   └── mysterious-letter.md
└── scenes/               ← (managed at runtime, usually leave empty)

Character and NPC files

The filename becomes the slug (used in @-mentions and session data). The first # Heading becomes the display name.

# Elara Swift

## Background
A wandering ranger from the northern wastes...

## Stats
STR 12 | DEX 16 | CON 10 | INT 14 | WIS 13 | CHA 8

## Personality
Blunt, distrustful of authority, fiercely loyal once trust is earned.

MEMORY.md

Injected wholesale into every AI system prompt. Use it for:

  • Tone and setting guidance ("this is a gritty noir campaign, not high fantasy")
  • House rules and rulings
  • Things every character would know in-world
  • Recurring NPCs or factions
  • Session notes you want the AI to carry

Handouts

Handouts are documents the GM can "collect" during play — a letter, a map description, a book excerpt. Once collected, they appear in the AI context for all subsequent responses. Drop a new .md file into the handouts/ folder anytime; the server picks it up without a restart.


Environment Variables

All optional except ANTHROPIC_API_KEY.

Variable Default Description
ANTHROPIC_API_KEY Required. Your Anthropic API key.
ANTHROPIC_MODEL claude-sonnet-4-6 Claude model to use for all agent calls.
DEFAULT_CAMPAIGN Slug of the campaign to load on startup.
CAMPAIGN_ROOT ./campaigns Path to the directory containing campaigns.
DEV_INSTANT_RESPONSES false Set to true to skip timing delays (responses fire immediately). Useful when writing/testing.
PORT 3001 Backend HTTP port.
ALLOWED_ORIGIN http://localhost:5173 CORS origin whitelist for the frontend.
NODE_EXTRA_CA_CERTS Path to a corporate proxy CA cert if needed for outbound HTTPS.

Running the App

# Development (both servers, hot reload)
npm run dev

# Backend only
cd backend && npm run dev

# Frontend only
cd frontend && npm run dev

# Production build
cd frontend && npm run build
# Then serve frontend/dist as static files and run backend directly

With instant AI responses (good for testing content):

DEV_INSTANT_RESPONSES=true

Without that flag, characters respond with realistic delays (5–10 minutes for initial responses, 30 minutes for follow-ups) to simulate async play.


UI Walkthrough

Left Sidebar

  • Campaign name at the top.
  • Players section: shows all active players, their archetype, and status. Click Add Player to bring in a character. Click a player's status badge to cycle it. Poke fires an immediate AI response.
  • Handouts section: lists all handouts in the campaign, with a button to collect each one. Collected handouts are highlighted and flow into the AI context.
  • Scenes section: lists all scenes. Click to switch the active scene (filters the chat). The + button opens the scene creation modal.

Chat Log (center)

  • Messages scroll bottom-up (newest at bottom).
  • Click the pin icon on a message to keep it in the AI context indefinitely.
  • Click the edit icon (pencil) to edit a message inline.
  • Click the delete icon (trash) to remove it from the session.
  • Messages from outside the active scene are hidden — switch scenes or set no active scene to see everything.

GM Input (bottom center)

  • Type your message and press Enter (or the send button).
  • Type @ to trigger character autocomplete. Multiple tags allowed.
  • Type /r 2d6+3 to roll dice inline.
  • OOC: wrap anything in (parentheses) for out-of-character text.
  • Markdown works: **bold**, _italic_, etc.

Right Sidebar

  • Shows the full character sheet for each active player.
  • Rendered as markdown. Update the .md file and refresh to see changes.

Dice Rolling

Use /r EXPRESSION in the GM input. Examples:

Expression What it does
/r d20 Single d20
/r 2d6+3 Two d6, add 3
/r 4d6k3 Roll 4d6, keep highest 3 (standard stat roll)
/r 2d20dh1 Roll 2d20, drop highest (disadvantage)
/r 2d20kh1 Roll 2d20, keep highest (advantage)
/r 1d100 Percentile roll
/r d% Percentile (alias)
/r 4dF 4 Fudge dice (results: -1, 0, +1)
/r 2d6! Exploding d6s (reroll on max, add)
/r 1d6+1d4+2 Mixed dice with modifier

Results appear in the chat log with individual die results and the total.

Characters can also roll dice during their AI responses — the AI calls the same dice engine via a tool when it wants to make a check.


AI Character System

How prompts are built

When a GM message is sent, the backend builds a system prompt for each character that should respond. It contains, in order:

  1. Behavioral instructions — character should not assert outcomes, ask questions rather than declare, stay in character.
  2. Character sheet — full content of the character's .md file.
  3. Voice archetype — one of 12 Jungian archetypes (see below).
  4. Campaign memory — full contents of MEMORY.md.
  5. Rules files — all .md files in rules/.
  6. Scene NPCs — sheets for all NPCs in the current scene.
  7. Collected handouts — all collected handout documents.
  8. Session log — all pinned messages + the last 60 messages.

Prompt caching is applied to the system prompt (ephemeral cache, ~5 min TTL) to cut token costs significantly on rapid exchanges.

Jungian archetypes

Each player is randomly assigned one of 12 archetypes when added to the session. The archetype shapes voice and tone but not character facts. Archetypes:

  • The Innocent — hopeful, trusting, looks for good in others
  • The Orphan/Everyman — pragmatic, grounded, relatable
  • The Hero — courageous, driven, rises to challenges
  • The Caregiver — nurturing, self-sacrificing, protective
  • The Explorer — restless, curious, values freedom
  • The Rebel/Outlaw — challenges rules, disruptive, provocative
  • The Lover — passionate, committed, seeks connection
  • The Creator — imaginative, expressive, values originality
  • The Jester — playful, irreverent, uses humor to cope
  • The Sage — analytical, objective, seeks truth
  • The Magician — transformative, visionary, sees patterns
  • The Ruler — authoritative, responsible, takes control

To change a character's archetype, edit SESSION.json and update the archetype field in the players array.

Response timing

Condition Dev (DEV_INSTANT_RESPONSES=true) Production
Initial response (Round 1) Instant 5–10 min random delay
Follow-up response (Round 2) 200ms 30 min delay

Round 1 fires for characters who were directly addressed or when nobody has responded yet. Round 2 fires for characters who were mentioned by another character in Round 1.

When characters respond

A character responds when:

  • They were @-tagged in the GM message.
  • Their name appears in the message body.
  • No one has responded yet (someone should react).
  • Another Round 1 character mentioned them by name.

Characters with status inactive or dead are skipped automatically.

Manual poke

Click Poke next to any player to force an immediate response, bypassing timing and the "should respond" heuristic.


Adding Features

New backend API endpoint

  1. Create backend/routes/myfeature.js:
import { Router } from 'express';
const router = Router();

router.post('/myfeature/action', async (req, res) => {
  // do something
  res.json({ ok: true });
});

export default router;
  1. Mount it in backend/index.js:
import myFeatureRoutes from './routes/myfeature.js';
app.use('/api', myFeatureRoutes);
  1. If state changed, broadcast over SSE:
import { emit } from '../lib/eventBus.js';
emit('myEventType', { payload });
  1. In frontend/src/App.jsx, add a handler in the useEffect SSE listener:
case 'myEventType':
  setState(prev => ({ ...prev, ...data }));
  break;

New frontend component

  1. Create frontend/src/components/MyComponent.jsx (and MyComponent.css).
  2. Import into the parent (usually App.jsx or a sidebar).
  3. Use fetch('/api/...') for API calls — Vite proxies them to the backend.
  4. Pass data down via props; pass callbacks up via props.

Modify AI behavior

All AI logic lives in backend/lib/agentRunner.js. Key things to change:

  • BEHAVIOR_INSTRUCTION — the core rules every character follows.
  • buildSystem() — controls what context gets injected and in what order.
  • Response timing constantsROUND1_DELAY_MIN/MAX, ROUND2_DELAY.
  • shouldRespond() — heuristic for whether a character chimes in.
  • Tool definitions — the dice tool is defined here; add more tools to give characters new capabilities.

Add a new game system

  1. Create campaigns/my-campaign/rules/new-system.md with the rules in markdown.
  2. It's automatically loaded and injected into every AI prompt.
  3. Characters will follow those rules in responses.

Add evidence/handouts mid-session

  1. Write the document as a .md file in campaigns/my-campaign/handouts/.
  2. The server detects it immediately (no restart).
  3. The GM clicks Collect in the left sidebar to add it to the AI context.

Change a character's voice

Edit the archetype field in SESSION.json for the relevant player entry, using one of the 12 archetype names exactly as listed above. Takes effect on the next AI call.

Swap the Claude model

ANTHROPIC_MODEL=claude-opus-4-7

Or edit agentRunner.js directly to use different models per character type.


Key Files Quick Reference

File What to touch it for
backend/lib/agentRunner.js AI character behavior, prompts, timing, tools
backend/routes/session.js Core game mechanics (messages, scenes, pins, handouts)
backend/lib/archetypes.js Character voice templates
backend/lib/campaign.js How campaign files are read and watched
backend/lib/session.js SESSION.json read/write logic
frontend/src/App.jsx All frontend state and SSE listener
frontend/src/components/GMInput.jsx Dice, @-tagging, message sending
frontend/src/components/Message.jsx Message rendering, edit, delete, pin
shared/dice.js Dice notation parser — used by frontend display AND backend tool
campaigns.config.json Register new campaigns here
.env API keys and local config

Data Models

SESSION.json

{
  "messages": [
    {
      "id": "uuid",
      "sender": "Elara Swift",
      "senderSlug": "elara-swift",
      "senderType": "pc",
      "body": "I examine the lock closely.",
      "timestamp": "2026-01-15T20:34:00.000Z",
      "sceneId": "uuid",
      "pinned": false,
      "edited": false
    }
  ],
  "players": [
    {
      "characterSlug": "elara-swift",
      "status": "active",
      "archetype": "The Explorer",
      "addedAt": "2026-01-15T19:00:00.000Z"
    }
  ],
  "scenes": [
    {
      "id": "uuid",
      "title": "The Tavern",
      "participants": [
        { "slug": "elara-swift", "type": "pc" },
        { "slug": "innkeeper", "type": "npc" }
      ],
      "sessionZero": false,
      "seats": [],
      "createdAt": "2026-01-15T19:05:00.000Z"
    }
  ],
  "activeSceneId": "uuid",
  "collectedHandouts": ["mysterious-letter"],
  "pinnedMessageIds": ["uuid-of-pinned-message"]
}

Character markdown

# Character Name

## Background
...

## Stats
...

## Personality
...

The filename (minus .md) becomes the slug. The first # Heading is the display name.


API Reference

Campaign

Method Path Description
GET /api/campaign Campaign name, all characters/NPCs/rules/handouts
GET /api/tags All character and NPC slugs and names (for autocomplete)

Session

Method Path Description
GET /api/session Full current session state
POST /api/session/message Send GM message, triggers AI responses
PATCH /api/session/message/:id Edit message body
DELETE /api/session/message/:id Delete message
POST /api/session/player Add a player by characterSlug
DELETE /api/session/player/:slug Remove a player
PATCH /api/session/player/:slug/status Update player status
POST /api/session/scene Create new scene
POST /api/session/scene/:id/activate Switch active scene
POST /api/session/scene/:id/participant Add participant to scene
POST /api/session/pin/:messageId Toggle pin on a message
POST /api/session/handout/:slug/collect Collect a handout

Agent

Method Path Description
POST /api/agent/poke/:slug Force character to respond immediately

Events (SSE)

Connect to GET /api/events to receive a stream of events. Each event has a type and a JSON data payload. The connection sends a heartbeat every 25 seconds.


Troubleshooting

AI responses never arrive

  • Check that ANTHROPIC_API_KEY is set and valid.
  • Check the backend terminal for API errors.
  • Try enabling DEV_INSTANT_RESPONSES=true to rule out timing issues.
  • Make sure the player status is active (not inactive or dead).

"Cannot connect to server" in the frontend

  • Is the backend running? Check for errors in the backend terminal.
  • Is the frontend proxying correctly? Check frontend/vite.config.js — target should be http://localhost:3001.

Character doesn't appear in roster

  • The .md file must be in campaigns/your-campaign/characters/.
  • The campaign must be registered in campaigns.config.json.
  • The DEFAULT_CAMPAIGN env var must match the slug.

SESSION.json is corrupted

  • The backend serializes writes to prevent conflicts, but a hard crash mid-write can corrupt it.
  • Open SESSION.json, check for truncation or invalid JSON.
  • Restore from git, or rebuild the minimal structure (see Local Setup).
  • The session can also be reset by replacing it with the minimal template.

Handout not appearing after dropping into folder

  • The file watcher (chokidar) may take a second or two.
  • Verify the file is in handouts/ (not a subdirectory).
  • Restart the backend if it still doesn't appear.

AI isn't following campaign rules

  • Confirm rules files are in rules/ inside the campaign directory.
  • Check MEMORY.md for any conflicting instructions.
  • Rules are injected once per prompt — very long rule documents may affect model attention. Consider splitting into focused files.

Messages appear in wrong scene

  • Messages are tagged with the active scene at send time.
  • If you switched scenes and then re-sent, the message belongs to the new scene.
  • Edit SESSION.json directly to correct the sceneId on a message.

Corporate network / proxy errors

  • Set NODE_EXTRA_CA_CERTS to the path of your corporate CA certificate (.pem format).

About

Local AI play-by-post tabletop RPG manager

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages