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:
- Embedded : Stored directly in the card's
character_bookfield orextensions.additional_lorebooks - 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_lorebooksextensions.world_infosextensions.linked_lorebooksextensions.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.
- Authorship : Each lorebook may have a different creator
- Updates : Linked lorebooks can be refreshed from source
- Modularity : Users can enable/disable individual lorebooks
- Clarity : Clear separation between character lore and world lore
The package provides parsing, extraction, and insertion - not down. Reasons:
- Simplicity : No HTTP dependencies, works in any environment
- Flexibility : Apps handle auth, caching, rate limits differently
- 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);