{"slug": "lorebook-package-documentation", "title": "Lorebook Package Documentation", "summary": "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.", "body_md": "**Package:** `@character-foundry/lorebook`\n**Environment:** Node.js and Browser\n\nThe `@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.\n\nA lorebook is a collection of **entries** that provide contextual information to AI models during roleplay. Each entry has:\n\n- **Keys/Triggers** : Words or phrases that activate the entry\n- **Content** : The information injected into the context\n- **Metadata** : Priority, position, enabled state, etc.\n\nCharacter cards can contain lorebooks in two ways:\n\n1. **Embedded** : Stored directly in the card's`character_book` field or`extensions.additional_lorebooks`\n2. **Linked** : Referenced by URL in card extensions, fetched from external sources (Chub, Risu, etc.)\n\nA key design principle: **cards can have multiple separate lorebooks**. We never \"smoosh\" them into one giant lorebook. This preserves:\n\n- Original authorship and sources\n- Update paths for linked content\n- Clean separation of concerns\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                        Card (CCv3Data)                          │\n├─────────────────────────────────────────────────────────────────┤\n│  data.character_book          → Primary embedded lorebook       │\n│  data.extensions.additional_lorebooks → Array of extra books    │\n│  data.extensions.chub.linked_lorebooks → Linked refs (URLs)     │\n│  data.extensions.world_infos  → More linked refs                │\n└─────────────────────────────────────────────────────────────────┘\n                                │\n                                ▼\n┌─────────────────────────────────────────────────────────────────┐\n│                    LorebookCollection                           │\n├─────────────────────────────────────────────────────────────────┤\n│  embedded: CCv3CharacterBook[]   ← Multiple embedded books      │\n│  linked: LinkedLorebook[]        ← Fetched external books       │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n| Module | Purpose | \n|---|---|\n| `parser.ts` | Detect format and normalize to CCv3 | \n| `extractor.ts` | Extract refs and separate entries by source | \n| `inserter.ts` | Stamp entries with source metadata, manage card lorebooks | \n| `handler.ts` | Format conversion, serialization, entry utilities | \n\n```\ntype LorebookFormat =\n  | 'ccv3'         // Standard CCv3 character_book\n  | 'sillytavern'  // SillyTavern world_info JSON\n  | 'agnai'        // Agnai lorebook format\n  | 'risu'         // RisuAI .risulorebook\n  | 'wyvern'       // Wyvern format\n  | 'unknown';\n```\n\nResult of parsing a standalone lorebook file:\n\n```\ninterface ParsedLorebook {\n  book: CCv3CharacterBook;      // Normalized to CCv3\n  originalFormat: LorebookFormat;\n  originalShape: unknown;        // Raw data for round-trip\n}\n```\n\nReference to a linked lorebook found in card extensions:\n\n```\ninterface LorebookRef {\n  url: string;          // Source URL\n  platform: string;     // 'chub' | 'risu' | 'janitor' | etc.\n  id?: string;          // Platform-specific ID\n  name?: string;        // Display name if available\n}\n```\n\nA fetched/downloaded linked lorebook with source tracking:\n\n```\ninterface LinkedLorebook {\n  source: string;       // Source URL\n  platform: string;     // Platform identifier\n  sourceId?: string;    // ID on source platform\n  fetchedAt: string;    // ISO timestamp\n  name?: string;        // Display name\n  book: CCv3CharacterBook;  // The actual content\n}\n```\n\nMetadata stamped on entries from linked lorebooks:\n\n```\ninterface EntrySourceMeta {\n  linkedFrom: string;       // Source URL\n  platform: string;         // Platform identifier\n  fetchedAt: string;        // When fetched\n  originalEntryId?: string; // Original entry name/ID\n  lorebookName?: string;    // Lorebook name for display\n}\n```\n\nStored in `entry.extensions.lorebookSource`.\n\nComplete collection of lorebooks from a card:\n\n```\ninterface LorebookCollection {\n  embedded: CCv3CharacterBook[];   // Directly in card\n  linked: LinkedLorebook[];        // From external sources\n}\n```\n\nParse a standalone lorebook file (JSON) and normalize to CCv3:\n\n``` js\nimport { parseLorebook, detectLorebookFormat } from '@character-foundry/lorebook';\n\n// Auto-detect format and normalize\nconst jsonBuffer = await fs.readFile('lorebook.json');\nconst { book, originalFormat, originalShape } = parseLorebook(jsonBuffer);\n\nconsole.log(`Detected format: ${originalFormat}`);\nconsole.log(`Entries: ${book.entries.length}`);\n\n// Or detect format without parsing\nconst format = detectLorebookFormat(JSON.parse(jsonBuffer.toString()));\n```\n\n**Supported formats:**\n\n| Format | Detection | \n|---|---|\n| CCv3 | `entries` array with`keys` /`content` | \n| SillyTavern | `entries` object keyed by uid, entries have`uid` /`key` /`content` | \n| Agnai | `kind: 'memory'` , entries have`keywords` /`entry` | \n| Risu | `type: 'risu'` or`ripiVersion` present | \n| Wyvern | `format: 'wyvern'` or`wyvern` object present | \n\nGet all lorebooks from a character card:\n\n``` js\nimport {\n  getLorebookCollection,\n  extractLorebookRefs,\n  extractLinkedEntries\n} from '@character-foundry/lorebook';\n\n// Get the full collection (embedded + linked)\nconst collection = getLorebookCollection(card);\n\nconsole.log(`Embedded lorebooks: ${collection.embedded.length}`);\nconsole.log(`Linked lorebooks: ${collection.linked.length}`);\n\n// List embedded lorebook names\nfor (const book of collection.embedded) {\n  console.log(`- ${book.name}: ${book.entries.length} entries`);\n}\n\n// Find linked lorebook references (URLs to fetch)\nconst refs = extractLorebookRefs(card);\nfor (const ref of refs) {\n  console.log(`Linked: ${ref.url} (${ref.platform})`);\n}\n```\n\n**Where linked refs are found:**\n\n- `extensions.chub.linked_lorebooks`\n- `extensions.world_infos`\n- `extensions.linked_lorebooks`\n- `extensions.ripiLinkedLorebooks`\n\nComplete workflow for adding a linked lorebook to a card:\n\n``` js\nimport {\n  extractLorebookRefs,\n  parseLorebook,\n  createLinkedLorebook,\n  addLinkedLorebookToCard,\n  removeLinkedEntriesBySource,\n} from '@character-foundry/lorebook';\n\n// 1. Find linked refs in card\nconst refs = extractLorebookRefs(card);\n\n// 2. App fetches the lorebook (not our job!)\nconst lorebookJson = await yourFetchFunction(refs[0].url);\n\n// 3. Parse and stamp with source metadata\nconst { book } = parseLorebook(lorebookJson);\nconst linked = createLinkedLorebook(\n  book,\n  refs[0].url,    // sourceUrl\n  refs[0].platform,\n  refs[0].id\n);\n\n// 4. Add to card (stored in extensions.additional_lorebooks)\nlet updatedCard = addLinkedLorebookToCard(card, linked);\n\n// Later: Remove linked entries when unlinking\nupdatedCard = removeLinkedEntriesBySource(updatedCard, refs[0].url);\n```\n\nWork with individual entries:\n\n``` js\nimport {\n  findEntriesByKeys,\n  findEntryByNameOrId,\n  updateEntry,\n  addEntry,\n  removeEntry,\n  reorderEntries,\n} from '@character-foundry/lorebook';\n\n// Search entries by trigger keys\nconst matches = findEntriesByKeys(book, ['dragon', 'fire'], {\n  caseSensitive: false,\n  matchAll: false,  // Any key matches (OR)\n});\n\n// Find specific entry\nconst entry = findEntryByNameOrId(book, 'Dragons');\nconst entryById = findEntryByNameOrId(book, 42);\n\n// Update an entry\nconst updated = updateEntry(book, 'Dragons', {\n  content: 'Updated dragon lore...',\n  enabled: true,\n});\n\n// Add new entry\nconst withNew = addEntry(book, {\n  keys: ['phoenix', 'fire bird'],\n  content: 'Phoenix lore here...',\n  enabled: true,\n  name: 'Phoenix',\n  priority: 10,\n});\n\n// Remove entry\nconst removed = removeEntry(book, 'Outdated Entry');\n\n// Reorder entries\nconst reordered = reorderEntries(book, [5, 2, 3, 1, 4]);\n```\n\nConvert between lorebook formats:\n\n``` js\nimport {\n  convertLorebook,\n  serializeLorebook,\n  serializeParsedLorebook,\n} from '@character-foundry/lorebook';\n\n// Convert CCv3 to SillyTavern format\nconst stFormat = convertLorebook(book, 'sillytavern');\n\n// Serialize to JSON string\nconst json = serializeLorebook(book, 'sillytavern', undefined, true);\n\n// Round-trip: preserve original format\nconst { book, originalFormat, originalShape } = parseLorebook(input);\n// ... modify book ...\nconst output = serializeParsedLorebook({ book, originalFormat, originalShape });\n```\n\n**Format-specific field preservation:**\n\nWhen converting, format-specific fields are preserved in `entry.extensions`:\n\n```\n// SillyTavern fields preserved in entry.extensions.sillytavern\n{\n  selectiveLogic: 0,\n  excludeRecursion: false,\n  probability: 100,\n  depth: 4,\n  // ... etc\n}\n\n// Agnai fields preserved in entry.extensions.agnai\n{\n  weight: 1,\n}\n```\n\nParse a standalone lorebook file. Auto-detects format and normalizes to CCv3.\n\nDetect the format of a lorebook JSON object without parsing.\n\nNormalize any lorebook format to CCv3 character_book structure.\n\nExtract linked lorebook references (URLs) from card extensions.\n\nSeparate embedded entries from linked entries using `extensions.lorebookSource`.\n\nGet all lorebooks from a card as a collection with embedded and linked separated.\n\nStamp all entries with source metadata for tracking.\n\nCreate a LinkedLorebook from a fetched book with source info.\n\nAdd a linked lorebook to card's `extensions.additional_lorebooks`.\n\nAdd an embedded lorebook. Sets as `character_book` if none exists, otherwise adds to `additional_lorebooks`.\n\nRemove a lorebook by name from card.\n\nRemove all entries that came from a specific source URL.\n\nReplace a lorebook in card (matches by name).\n\nRebuild card's lorebooks from a LorebookCollection.\n\nConvert CCv3 to another format. Preserves original shape for round-trip.\n\nSerialize a lorebook to JSON string.\n\nRound-trip a parsed lorebook back to its original format.\n\nMerge two lorebooks into one. **Use with caution** - design preference is to keep separate.\n\nFind entries matching search keys.\n\nOptions:\n\n- `caseSensitive` : boolean (default: false)\n- `matchAll` : boolean - require all keys match (default: false)\n\nFind entry by name string or numeric ID.\n\nUpdate an entry's fields.\n\nAdd a new entry with auto-generated ID.\n\nRemove an entry by ID or name.\n\nReorder entries by specifying ID/name order.\n\n1. **Authorship** : Each lorebook may have a different creator\n2. **Updates** : Linked lorebooks can be refreshed from source\n3. **Modularity** : Users can enable/disable individual lorebooks\n4. **Clarity** : Clear separation between character lore and world lore\n\nThe package provides parsing, extraction, and insertion - **not downloading**. Reasons:\n\n1. **Simplicity** : No HTTP dependencies, works in any environment\n2. **Flexibility** : Apps handle auth, caching, rate limits differently\n3. **No bloat** : Supporting 100 card sites would be unmaintainable\n\n**Your app's responsibility:**\n\n- Fetch lorebook from URL\n- Handle authentication\n- Manage caching\n- Deal with errors\n\n**Our responsibility:**\n\n- Parse the fetched data\n- Stamp with source metadata\n- Insert into card structure\n\nDiffing and merge conflict UI is complex and app-specific. Primitives provide:\n\n- Source tracking (so you know where entries came from)\n- Entry matching by name/id AND keys\n- Update/replace operations\n\n**Your app's responsibility:**\n\n- Show diff UI to users\n- Let users resolve conflicts\n- Call our update functions with resolved data\n\n``` js\nimport {\n  parseCard,\n  exportCard,\n} from '@character-foundry/loader';\nimport {\n  getLorebookCollection,\n  extractLorebookRefs,\n  parseLorebook,\n  createLinkedLorebook,\n  addLinkedLorebookToCard,\n  replaceLorebookInCard,\n  setLorebookCollection,\n  updateEntry,\n} from '@character-foundry/lorebook';\n\nclass LorebookEditor {\n  private card: CCv3Data;\n  private collection: LorebookCollection;\n\n  async load(buffer: Uint8Array) {\n    const { card } = parseCard(buffer);\n    this.card = card;\n    this.collection = getLorebookCollection(card);\n  }\n\n  // Refresh a linked lorebook from source\n  async refreshLinked(sourceUrl: string, fetchFn: (url: string) => Promise<Uint8Array>) {\n    const data = await fetchFn(sourceUrl);\n    const { book } = parseLorebook(data);\n\n    // Find existing linked lorebook\n    const existing = this.collection.linked.find(l => l.source === sourceUrl);\n    if (existing) {\n      // Update with fresh data\n      const linked = createLinkedLorebook(book, sourceUrl, existing.platform);\n      this.collection.linked = this.collection.linked.map(l =>\n        l.source === sourceUrl ? linked : l\n      );\n    }\n\n    // Rebuild card\n    this.card = setLorebookCollection(this.card, this.collection);\n  }\n\n  // Edit an entry in an embedded lorebook\n  editEntry(bookIndex: number, entryId: number, updates: Partial<CCv3LorebookEntry>) {\n    const book = this.collection.embedded[bookIndex];\n    const updated = updateEntry(book, entryId, updates);\n    this.collection.embedded[bookIndex] = updated;\n    this.card = setLorebookCollection(this.card, this.collection);\n  }\n\n  // Export\n  export(format: 'png' | 'charx'): Uint8Array {\n    return exportCard(this.card, [], { format });\n  }\n}\njs\nimport { parseLorebook, serializeLorebook } from '@character-foundry/lorebook';\n\n// Convert SillyTavern → CCv3\nconst stInput = await fs.readFile('world_info.json');\nconst { book } = parseLorebook(stInput);\nconst ccv3Output = JSON.stringify(book, null, 2);\nawait fs.writeFile('lorebook_ccv3.json', ccv3Output);\n\n// Convert CCv3 → SillyTavern\nconst ccv3Input = await fs.readFile('lorebook_ccv3.json');\nconst { book: ccv3Book, originalFormat, originalShape } = parseLorebook(ccv3Input);\nconst stOutput = serializeLorebook(ccv3Book, 'sillytavern');\nawait fs.writeFile('world_info_converted.json', stOutput);\n```\n\n", "url": "https://wpnews.pro/news/lorebook-package-documentation", "canonical_source": "https://github.com/character-foundry/character-foundry/blob/master/docs/lorebook.md", "published_at": "2026-10-25 00:00:57+00:00", "updated_at": "2026-10-25 00:12:35.954331+00:00", "lang": "en", "topics": ["ai-products", "developer-tools", "ai-tools"], "entities": ["@character-foundry/lorebook", "CCv3", "SillyTavern", "Agnai", "RisuAI", "Wyvern", "Chub", "Node.js"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/lorebook-package-documentation", "markdown": "https://wpnews.pro/news/lorebook-package-documentation.md", "text": "https://wpnews.pro/news/lorebook-package-documentation.txt", "jsonld": "https://wpnews.pro/news/lorebook-package-documentation.jsonld"}}