cd /news/ai-products/lorebook-package-documentation Β· home β€Ί topics β€Ί ai-products β€Ί article
[ARTICLE Β· art-157026] src=github.com β†— pub= topic=ai-products verified=true sentiment=Β· neutral

Lorebook Package Documentation

The @character-foundry/lorebook package for Node.js and browsers provides primitives to parse, extract and manage lorebooks across AI character card formats including CCv3, SillyTavern, Agnai, Risu and Wyvern. The package keeps a card's multiple lorebooks separate rather than merging them, preserving authorship, update paths for linked content and separation of concerns, and normalizes parsed books to CCv3 while retaining original raw data for round-trip conversion.

read8 min views1 publishedOct 25, 2026
Lorebook Package Documentation
Image: Michielbdejong (auto-discovered)

Package: @character-foundry/lorebook Environment: Node.js and Browser

The @character-foundry/lorebook package provides primitives for parsing, extracting, and managing lorebooks (also known as world info or memory books) across different AI character card formats.

A lorebook is a collection of entries that provide contextual information to AI models during roleplay. Each entry has:

  • Keys/Triggers : Words or phrases that activate the entry
  • Content : The information injected into the context
  • Metadata : Priority, position, enabled state, etc.

Character cards can contain lorebooks in two ways:

  1. Embedded : Stored directly in the card'scharacter_book field orextensions.additional_lorebooks
  2. Linked : Referenced by URL in card extensions, fetched from external sources (Chub, Risu, etc.)

A key design principle: cards can have multiple separate lorebooks. We never "smoosh" them into one giant lorebook. This preserves:

  • Original authorship and sources
  • Update paths for linked content
  • Clean separation of concerns
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Card (CCv3Data)                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  data.character_book          β†’ Primary embedded lorebook       β”‚
β”‚  data.extensions.additional_lorebooks β†’ Array of extra books    β”‚
β”‚  data.extensions.chub.linked_lorebooks β†’ Linked refs (URLs)     β”‚
β”‚  data.extensions.world_infos  β†’ More linked refs                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    LorebookCollection                           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  embedded: CCv3CharacterBook[]   ← Multiple embedded books      β”‚
β”‚  linked: LinkedLorebook[]        ← Fetched external books       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Module Purpose
parser.ts Detect format and normalize to CCv3
extractor.ts Extract refs and separate entries by source
inserter.ts Stamp entries with source metadata, manage card lorebooks
handler.ts Format conversion, serialization, entry utilities
type LorebookFormat =
  | 'ccv3'         // Standard CCv3 character_book
  | 'sillytavern'  // SillyTavern world_info JSON
  | 'agnai'        // Agnai lorebook format
  | 'risu'         // RisuAI .risulorebook
  | 'wyvern'       // Wyvern format
  | 'unknown';

Result of parsing a standalone lorebook file:

interface ParsedLorebook {
  book: CCv3CharacterBook;      // Normalized to CCv3
  originalFormat: LorebookFormat;
  originalShape: unknown;        // Raw data for round-trip
}

Reference to a linked lorebook found in card extensions:

interface LorebookRef {
  url: string;          // Source URL
  platform: string;     // 'chub' | 'risu' | 'janitor' | etc.
  id?: string;          // Platform-specific ID
  name?: string;        // Display name if available
}

A fetched/downloaded linked lorebook with source tracking:

interface LinkedLorebook {
  source: string;       // Source URL
  platform: string;     // Platform identifier
  sourceId?: string;    // ID on source platform
  fetchedAt: string;    // ISO timestamp
  name?: string;        // Display name
  book: CCv3CharacterBook;  // The actual content
}

Metadata stamped on entries from linked lorebooks:

interface EntrySourceMeta {
  linkedFrom: string;       // Source URL
  platform: string;         // Platform identifier
  fetchedAt: string;        // When fetched
  originalEntryId?: string; // Original entry name/ID
  lorebookName?: string;    // Lorebook name for display
}

Stored in entry.extensions.lorebookSource.

Complete collection of lorebooks from a card:

interface LorebookCollection {
  embedded: CCv3CharacterBook[];   // Directly in card
  linked: LinkedLorebook[];        // From external sources
}

Parse a standalone lorebook file (JSON) and normalize to CCv3:

import { parseLorebook, detectLorebookFormat } from '@character-foundry/lorebook';

// Auto-detect format and normalize
const jsonBuffer = await fs.readFile('lorebook.json');
const { book, originalFormat, originalShape } = parseLorebook(jsonBuffer);

console.log(`Detected format: ${originalFormat}`);
console.log(`Entries: ${book.entries.length}`);

// Or detect format without parsing
const format = detectLorebookFormat(JSON.parse(jsonBuffer.toString()));

Supported formats:

Format Detection
CCv3 entries array withkeys /content
SillyTavern entries object keyed by uid, entries haveuid /key /content
Agnai kind: 'memory' , entries havekeywords /entry
Risu type: 'risu' orripiVersion present
Wyvern format: 'wyvern' orwyvern object present

Get all lorebooks from a character card:

import {
  getLorebookCollection,
  extractLorebookRefs,
  extractLinkedEntries
} from '@character-foundry/lorebook';

// Get the full collection (embedded + linked)
const collection = getLorebookCollection(card);

console.log(`Embedded lorebooks: ${collection.embedded.length}`);
console.log(`Linked lorebooks: ${collection.linked.length}`);

// List embedded lorebook names
for (const book of collection.embedded) {
  console.log(`- ${book.name}: ${book.entries.length} entries`);
}

// Find linked lorebook references (URLs to fetch)
const refs = extractLorebookRefs(card);
for (const ref of refs) {
  console.log(`Linked: ${ref.url} (${ref.platform})`);
}

Where linked refs are found:

  • extensions.chub.linked_lorebooks
  • extensions.world_infos
  • extensions.linked_lorebooks
  • extensions.ripiLinkedLorebooks

Complete workflow for adding a linked lorebook to a card:

import {
  extractLorebookRefs,
  parseLorebook,
  createLinkedLorebook,
  addLinkedLorebookToCard,
  removeLinkedEntriesBySource,
} from '@character-foundry/lorebook';

// 1. Find linked refs in card
const refs = extractLorebookRefs(card);

// 2. App fetches the lorebook (not our job!)
const lorebookJson = await yourFetchFunction(refs[0].url);

// 3. Parse and stamp with source metadata
const { book } = parseLorebook(lorebookJson);
const linked = createLinkedLorebook(
  book,
  refs[0].url,    // sourceUrl
  refs[0].platform,
  refs[0].id
);

// 4. Add to card (stored in extensions.additional_lorebooks)
let updatedCard = addLinkedLorebookToCard(card, linked);

// Later: Remove linked entries when unlinking
updatedCard = removeLinkedEntriesBySource(updatedCard, refs[0].url);

Work with individual entries:

import {
  findEntriesByKeys,
  findEntryByNameOrId,
  updateEntry,
  addEntry,
  removeEntry,
  reorderEntries,
} from '@character-foundry/lorebook';

// Search entries by trigger keys
const matches = findEntriesByKeys(book, ['dragon', 'fire'], {
  caseSensitive: false,
  matchAll: false,  // Any key matches (OR)
});

// Find specific entry
const entry = findEntryByNameOrId(book, 'Dragons');
const entryById = findEntryByNameOrId(book, 42);

// Update an entry
const updated = updateEntry(book, 'Dragons', {
  content: 'Updated dragon lore...',
  enabled: true,
});

// Add new entry
const withNew = addEntry(book, {
  keys: ['phoenix', 'fire bird'],
  content: 'Phoenix lore here...',
  enabled: true,
  name: 'Phoenix',
  priority: 10,
});

// Remove entry
const removed = removeEntry(book, 'Outdated Entry');

// Reorder entries
const reordered = reorderEntries(book, [5, 2, 3, 1, 4]);

Convert between lorebook formats:

import {
  convertLorebook,
  serializeLorebook,
  serializeParsedLorebook,
} from '@character-foundry/lorebook';

// Convert CCv3 to SillyTavern format
const stFormat = convertLorebook(book, 'sillytavern');

// Serialize to JSON string
const json = serializeLorebook(book, 'sillytavern', undefined, true);

// Round-trip: preserve original format
const { book, originalFormat, originalShape } = parseLorebook(input);
// ... modify book ...
const output = serializeParsedLorebook({ book, originalFormat, originalShape });

Format-specific field preservation:

When converting, format-specific fields are preserved in entry.extensions:

// SillyTavern fields preserved in entry.extensions.sillytavern
{
  selectiveLogic: 0,
  excludeRecursion: false,
  probability: 100,
  depth: 4,
  // ... etc
}

// Agnai fields preserved in entry.extensions.agnai
{
  weight: 1,
}

Parse a standalone lorebook file. Auto-detects format and normalizes to CCv3.

Detect the format of a lorebook JSON object without parsing.

Normalize any lorebook format to CCv3 character_book structure.

Extract linked lorebook references (URLs) from card extensions.

Separate embedded entries from linked entries using extensions.lorebookSource.

Get all lorebooks from a card as a collection with embedded and linked separated.

Stamp all entries with source metadata for tracking.

Create a LinkedLorebook from a fetched book with source info.

Add a linked lorebook to card's extensions.additional_lorebooks.

Add an embedded lorebook. Sets as character_book if none exists, otherwise adds to additional_lorebooks.

Remove a lorebook by name from card.

Remove all entries that came from a specific source URL.

Replace a lorebook in card (matches by name).

Rebuild card's lorebooks from a LorebookCollection.

Convert CCv3 to another format. Preserves original shape for round-trip.

Serialize a lorebook to JSON string.

Round-trip a parsed lorebook back to its original format.

Merge two lorebooks into one. Use with caution - design preference is to keep separate.

Find entries matching search keys.

Options:

  • caseSensitive : boolean (default: false)
  • matchAll : boolean - require all keys match (default: false)

Find entry by name string or numeric ID.

Update an entry's fields.

Add a new entry with auto-generated ID.

Remove an entry by ID or name.

Reorder entries by specifying ID/name order.

  1. Authorship : Each lorebook may have a different creator
  2. Updates : Linked lorebooks can be refreshed from source
  3. Modularity : Users can enable/disable individual lorebooks
  4. Clarity : Clear separation between character lore and world lore

The package provides parsing, extraction, and insertion - not down. Reasons:

  1. Simplicity : No HTTP dependencies, works in any environment
  2. Flexibility : Apps handle auth, caching, rate limits differently
  3. No bloat : Supporting 100 card sites would be unmaintainable

Your app's responsibility:

  • Fetch lorebook from URL
  • Handle authentication
  • Manage caching
  • Deal with errors

Our responsibility:

  • Parse the fetched data
  • Stamp with source metadata
  • Insert into card structure

Diffing and merge conflict UI is complex and app-specific. Primitives provide:

  • Source tracking (so you know where entries came from)
  • Entry matching by name/id AND keys
  • Update/replace operations

Your app's responsibility:

  • Show diff UI to users
  • Let users resolve conflicts
  • Call our update functions with resolved data
import {
  parseCard,
  exportCard,
} from '@character-foundry/';
import {
  getLorebookCollection,
  extractLorebookRefs,
  parseLorebook,
  createLinkedLorebook,
  addLinkedLorebookToCard,
  replaceLorebookInCard,
  setLorebookCollection,
  updateEntry,
} from '@character-foundry/lorebook';

class LorebookEditor {
  private card: CCv3Data;
  private collection: LorebookCollection;

  async load(buffer: Uint8Array) {
    const { card } = parseCard(buffer);
    this.card = card;
    this.collection = getLorebookCollection(card);
  }

  // Refresh a linked lorebook from source
  async refreshLinked(sourceUrl: string, fetchFn: (url: string) => Promise<Uint8Array>) {
    const data = await fetchFn(sourceUrl);
    const { book } = parseLorebook(data);

    // Find existing linked lorebook
    const existing = this.collection.linked.find(l => l.source === sourceUrl);
    if (existing) {
      // Update with fresh data
      const linked = createLinkedLorebook(book, sourceUrl, existing.platform);
      this.collection.linked = this.collection.linked.map(l =>
        l.source === sourceUrl ? linked : l
      );
    }

    // Rebuild card
    this.card = setLorebookCollection(this.card, this.collection);
  }

  // Edit an entry in an embedded lorebook
  editEntry(bookIndex: number, entryId: number, updates: Partial<CCv3LorebookEntry>) {
    const book = this.collection.embedded[bookIndex];
    const updated = updateEntry(book, entryId, updates);
    this.collection.embedded[bookIndex] = updated;
    this.card = setLorebookCollection(this.card, this.collection);
  }

  // Export
  export(format: 'png' | 'charx'): Uint8Array {
    return exportCard(this.card, [], { format });
  }
}
js
import { parseLorebook, serializeLorebook } from '@character-foundry/lorebook';

// Convert SillyTavern β†’ CCv3
const stInput = await fs.readFile('world_info.json');
const { book } = parseLorebook(stInput);
const ccv3Output = JSON.stringify(book, null, 2);
await fs.writeFile('lorebook_ccv3.json', ccv3Output);

// Convert CCv3 β†’ SillyTavern
const ccv3Input = await fs.readFile('lorebook_ccv3.json');
const { book: ccv3Book, originalFormat, originalShape } = parseLorebook(ccv3Input);
const stOutput = serializeLorebook(ccv3Book, 'sillytavern');
await fs.writeFile('world_info_converted.json', stOutput);
── more in #ai-products 4 stories Β· sorted by recency
── more on @@character-foundry/lorebook 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/lorebook-package-doc…] indexed:0 read:8min 2026-10-25 Β· β€”