🧩 JavaScript API

Published 2026-03-12

Since v0.75.0

📖 Overview

The Tavo JavaScript API is a set of JavaScript interfaces for players and creators, enabling users to access powerful functionalities and high playability when JavaScript support is enabled.

⚙️ Variables

Variables are used to store data. Native JavaScript variables only survive within the page and are lost after refresh, so we provide a set of variable APIs to help users store data long-term.

Get a Variable

tavo.get(<name>[, <scope>])

For example:

let age = tavo.get('age')  // Get "age" from chat variables
let bestScore = tavo.get('bestScore', 'global')  // Get global best score
let lover = tavo.get('lover', 'character')  // Get the current character's lover

Set a Variable

tavo.set(<name>, <value>[, <scope>])

For example:

tavo.set('age', 16)  // Set chat variable age = 16
tavo.set('Lily_lover', 'Colin', 'global')  // Set a global variable: Lily's lover is Colin
tavo.set('status', { hp: 100, mp: 32, location: 'Cave' })  // Set current chat status: HP 100, MP 32, location Cave

Delete a Variable

tavo.unset(<name>[, <scope>])

For example:

tavo.set('age', 16)  // age = 16
tavo.unset('age')  // age = null

Variable Paths

When operating on variables, path-style access is supported, for example:

tavo.set('status', { hp: 100, mp: 50 })
tavo.get('status.hp')  // 100
tavo.unset('status.hp')  // status = { mp: 50 }

Use Variables in Prompts

You can send variables to the model through prompts by using macros:

{{getvar::<name>}} gets a variable (scope: chat, current chat) {{getglobalvar::<name>}} gets a variable (scope: global)

For example:

{{char}} has a new name: {{getvar::name}}
{{user}} current HP: {{getvar::status.hp}}
Global all-time best score: {{getglobalvar::highestScore}}

For more variable macros, see

#变量
/welcome/supported-macros/#变量

Example

Copy the following into any bubble to see it in action.

<h3>Variable API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 80px; max-height: 300px; overflow-y: auto;"></pre>
<p style="margin: 4px 0; font-size: 12px; opacity: .6;">chat scope (saved with current chat)</p>
<div style="display: grid; grid-template-columns: auto auto auto;">
  <button onclick="varGet()">tavo.get (chat)</button>
  <button onclick="varSet()">tavo.set (chat)</button>
  <button onclick="varUnset()">tavo.unset (chat)</button>
</div>
<p style="margin: 8px 0 4px; font-size: 12px; opacity: .6;">global scope (saved across chats and still exists after switching conversations)</p>
<div style="display: grid; grid-template-columns: auto auto auto;">
  <button onclick="varGetGlobal()">tavo.get (global)</button>
  <button onclick="varSetGlobal()">tavo.set (global)</button>
  <button onclick="varUnsetGlobal()">tavo.unset (global)</button>
</div>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };
  async function varGet() {
    const val = await tavo.get('status');
    log('chat / status =', val ? val : '(not set yet)');
  }
  async function varSet() {
    await tavo.set('status', { hp: 100, mp: 32, location: 'Cave' });
    tavo.utils.toast('Set chat / status');
  }
  async function varUnset() {
    await tavo.unset('status');
    tavo.utils.toast('Deleted chat / status');
  }
  async function varGetGlobal() {
    const val = await tavo.get('globalScore', 'global');
    log('global / globalScore =', val ? val : '(not set yet)');
  }
  async function varSetGlobal() {
    const s = await tavo.get('globalScore', 'global');
    const score = (s ? s : 0) + 1;
    await tavo.set('globalScore', score, 'global');
    tavo.utils.toast(`globalScore +1, current: ${score}`);
  }
  async function varUnsetGlobal() {
    await tavo.unset('globalScore', 'global');
    tavo.utils.toast('Deleted global / globalScore');
  }
</script>

💬 Messages

Since v0.78.0

You can use this interface to read or modify messages. All message interfaces are tavo.message.<method>(...).

Find Messages

await tavo.message.find(<indexRange>[, <filter>])

Finds messages by floor range indexRange and filter filter, and returns an array:

indexRange type number | array: floor range

  • When number:
    1. Gets the message at the specified floor
    2. Floors start from 0: first message is 0, second is 1, and so on
    3. Negative numbers count from the end: -1 is the last message, -2 is the second last, and so on
  • When array:
    1. [start, end], for example [2, 4], gets messages 2, 3, and 4 (inclusive)
    2. [start] means from start to the end
    3. [0, end] means from floor 0 to end
    4. [] | null | undefined means all floors
  • In all cases, this interface always returns an array. If a specified floor does not exist, it returns [].

filter type object: filter conditions

  • role type string: filter by role, optional values (default: all):
    1. 'system' system messages
    2. 'assistant' character messages
    3. 'user' user messages
  • hidden type boolean: filter by hidden flag, optional values (default: all):
    1. true only hidden messages
    2. false only non-hidden messages
  • characters type array: character ID array, only keeps messages sent by these characters

Message object format:

{
  id: 2338,  // Message ID
  characterId: 34,  // Character ID (assistant messages only)
  content: 'Hello!',  // Message content
  hidden: false,  // Whether this is a hidden message
  role: 'assistant'  // Message role
}

For example:

await tavo.message.find(2)  // Get the 3rd message
await tavo.message.find([3, 100])  // Get messages 3-100; if only 50 exist, returns 3-50
await tavo.message.find(-1, { role: 'user' })  // Last user message
await tavo.message.find([10], { hidden: false })  // Non-hidden messages with floor >= 10

Get a Single Message

await tavo.message.get(<messageId>)

Gets one message by message ID. Returns null if the ID is invalid or the message does not exist.

let msg = await tavo.message.get(2338)  // Get message with ID 2338

Get Current Message

await tavo.message.current()

Gets the message object for the message where this code is executed. Fields are the same as "Message object format" above and tavo.message.get.

Typical use cases: read role info on this message (tavo.character.get(currentMessage.characterId)), or modify this message (tavo.message.update).

const self = await tavo.message.current()
console.log(self)

Get Message Count

await tavo.message.count()

Gets the total number of messages in the current chat (including hidden messages). Commonly used to locate the last floor: first floor is 0, last floor is messageCount - 1.

let lastIndex = await tavo.message.count() - 1
console.log(lastIndex)

Append a Message

await tavo.message.append(<message>)

Appends a message to the end of the current chat. Returns the new message ID on success, or null on failure.

message type object, common fields:

  • content type string: message content (required)
  • role type string: 'assistant' | 'user' (defaults to 'assistant')
  • characterId type number: when role = 'assistant', you can specify the speaker character ID (optional in 1-on-1 chat, required in group chat)
  • hidden type boolean: whether hidden (default false)

Note:

  1. If role = 'assistant' and characterId is omitted, the character is auto-inferred from current session context.
  2. If no valid character can be inferred, or the character does not belong to current chat, creation fails and returns null.

For example:

let newId = await tavo.message.append({
  role: 'assistant',
  characterId: 34,
  content: 'This is an appended message',
  hidden: false,
})

In 1-on-1 chats, creating a visible message can be simplified:

let newId = await tavo.message.append({
  content: 'This is an appended message. role defaults to assistant; in 1-on-1 chats, character is auto-inferred; hidden defaults to false',
})

Update a Message

await tavo.message.update(<message>)

Updates an existing message by message ID. Returns the message ID on success, or null on failure.

message type object, common fields:

  • id type number: message ID to update (required)
  • content type string: updated message content (required)
  • reasoning type string: reasoning content (optional, empty string clears it)
  • hidden type boolean: whether hidden (optional, defaults to false)
const lastMessage = (await tavo.message.find(-1))[0]  // Get the last message
lastMessage.content = 'Updated content'
lastMessage.reasoning = 'Optional reasoning content'
lastMessage.hidden = true  // Mark as hidden
await tavo.message.update(lastMessage)  // Update the last message

Delete a Message

await tavo.message.delete(<messageId>)

Deletes a message by message ID. Returns the deleted message ID on success, or null on failure.

const count = await tavo.message.count();  // Get total message count
const midIndex = Math.floor(count / 2);
const midMessage = (await tavo.message.find(midIndex))[0]  // Get a middle message
await tavo.message.delete(midMessage.id)  // Delete that middle message

🗨️ Chat

You can use this interface to get information about the current chat. All chat interfaces are tavo.chat.<method>(...).

Get Current Chat

await tavo.chat.current()

Gets the currently active chat information. Returns null if there is no active chat.

For example:

let chat = await tavo.chat.current()
console.log(chat.name)        // Print current chat name
console.log(chat.characters[0]?.name)  // Print first character name
console.log(chat.persona?.name)        // Print current user persona name (if any)

Update Current Chat

await tavo.chat.update(<chat>)

Updates the current chat.

Updatable fields:

  • name: chat title
  • characters: character ID array (directly replaces current character list)
  • persona: user persona ID
await tavo.chat.update({
  name: 'New Chat Title',
  characters: [12, 34],
  persona: 5,
})

Note: This interface only updates the current chat, and does not support updating other sessions by chat ID.

Chat Object Fields

The chat object returned by current commonly includes:

{
  id: 1,                    // Chat ID
  name: 'Conversation with Alice',    // Chat name
  characters: [             // Character summary list in this chat
    {
      id: 12,
      name: 'Alice',
      avatar: 'alice.png'
    },
    {
      id: 7,
      name: 'Lee',
      avatar: 'lee.png'
    },
  ],
  persona: {                // Current user persona summary (can be null)
    id: 5,
    name: 'Default User Persona',
  },
  preset: {                 // Current preset summary
    id: 9,
    name: 'Default Preset',
  },
  lorebooks: [{
    id: 17,
    name: 'Sleepless City',
  }],
  regexes: [{               // Enabled regex summary list
    id: 3,
    name: 'Remove Stage Directions',
  }],
}

Example

Copy the following into any bubble to see it in action.

<h3>Chat API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 80px; max-height: 300px; overflow-y: auto;"></pre>
<div style="display: grid; grid-template-columns: auto auto;">
  <button onclick="chatCurrent()">tavo.chat.current</button>
  <button onclick="chatRename()">Rename current chat</button>
</div>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };
  async function chatCurrent() {
    const chat = await tavo.chat.current();
    log(chat);
  }
  async function chatRename() {
    const chat = await tavo.chat.current();
    if (!chat) return tavo.utils.toast('No active chat right now');
    const newName = prompt('Enter the new chat name', chat.name);
    if (!newName || newName === chat.name) return;
    chat.name = newName;
    await tavo.chat.update(chat);
    tavo.utils.toast(`Renamed to: ${newName}`);
  }
</script>

🧙 Characters

You can use this interface to manage characters. All character interfaces are tavo.character.<method>(...).

Get All Character Summaries

await tavo.character.all()

Returns an array of character summary objects (each item only contains summary fields such as id, name, and avatar):

let chars = await tavo.character.all()
console.log(chars[0].id)     // e.g. 12
console.log(chars[0].avatar) // e.g. "chara/alice.png"
console.log(chars[0].name)   // e.g. "Alice"

Get a Single Character

await tavo.character.get(<characterId>)

Gets a character object by character ID. Returns null if not found.

let char = await tavo.character.get(12)
if (char) {
  console.log(char.name)
}

Find Characters by Name

await tavo.character.find(<name>[, <options>])

Finds characters by name and returns an array of character objects. options.match supports: 'exact' | 'prefix' | 'suffix' | 'contains' (default: 'exact').

let chars = await tavo.character.find('Alice')
let chars2 = await tavo.character.find('Ali', { match: 'prefix' })
console.log(chars.length)

Create a Character

await tavo.character.create(<character>)

Creates a character and returns the new character ID. character.name and character.first_mes are required.

let id = await tavo.character.create({
  name: 'Alice',
  first_mes: 'Hi, I am Alice.',
  description: 'A gentle guide',
})

Update a Character

await tavo.character.update(<character>)

Updates a character and returns the character ID. character.id, character.name, and character.first_mes are required.

await tavo.character.update({
  id: 12,
  name: 'Alice',
  first_mes: 'Hi, I am Alice.',
  personality: 'Patient, detail-oriented',
})

Delete a Character

await tavo.character.delete(<characterId>)

Deletes a character by character ID:

await tavo.character.delete(12)
await tavo.character.delete(char)  // char must be a character object that includes id

Character Object Fields

Character objects (returned by get / find) commonly include:

{
  id: 12,  // Unique character ID
  avatar: 'xxx.png',  // Character avatar image URL or path
  name: 'Alice',  // Character name (required)
  description: '...',  // Character intro/description
  first_mes: '...',  // Character greeting (required)
  personality: '...',  // Character personality description
  scenario: '...',  // Applicable scenario/use-case description
  mes_example: '...',  // Message examples, separated by <START>
  creator_notes: '...',  // Creator notes or additional remarks
  system_prompt: '...',  // System prompt
  post_history_instructions: '...',  // Extra guidance after message history
  alternate_greetings: ['...'],  // Alternate greetings for this character
  tags: ['guide'],  // Character tags for classification/search
  creator: 'Colin',  // Creator username or nickname
  character_version: '1.0',  // Character version
  nickname: 'Ali',  // Character nickname/alias; if set, it replaces name in {{char}} output
  group_only_greetings: ['...'],  // Greetings only used in group chat
  creation_date: new Date('2026-03-05T10:20:30.000Z'),  // Creation time (Date object)
  modification_date: new Date('2026-03-05T11:30:00.000Z'),  // Last modified time (Date object)
}

Note: Creating, updating, and deleting characters will show a confirmation dialog. If the user cancels, the operation will not take effect.

Example

Copy the following into any bubble to see it in action.

<h3>Character API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 100px; max-height: 400px; overflow-y: auto;"></pre>
<div style="display: grid; grid-template-columns: auto auto auto auto auto auto;">
  <button onclick="charAll()">tavo.character.all</button>
  <button onclick="charGet()">tavo.character.get</button>
  <button onclick="charFind()">tavo.character.find</button>
  <button onclick="charCreate()">tavo.character.create</button>
  <button onclick="charUpdate()">tavo.character.update</button>
  <button onclick="charDelete()">tavo.character.delete</button>
</div>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };
  async function charAll() {
    const chars = await tavo.character.all();
    log(chars);
  }
  async function charGet() {
    const id = prompt('Enter a character ID (you can get it from tavo.character.all())');
    if (!id) return;
    const char = await tavo.character.get(Number(id));
    log(char);
  }
  async function charFind() {
    const name = prompt('Enter the character name to search');
    if (!name) return;
    const chars = await tavo.character.find(name);
    log(chars);
  }
  async function charCreate() {
    const id = await tavo.character.create({
      name: 'Demo Character',
      first_mes: 'Hi, I am a demo character!',
      description: 'This is a test character created via tavo.character.create.',
    });
    if (!id) return;
    const char = await tavo.character.get(id);
    log(char);
  }
  async function charUpdate() {
    const chars = await tavo.character.find('Demo Character');
    if (!chars.length) return tavo.utils.toast('Click tavo.character.create first to create a character');
    const char = chars[0];
    const newDesc = prompt('Edit character description:', char.description);
    if (!newDesc) return;
    char.description = newDesc;
    await tavo.character.update(char);
    log(await tavo.character.get(char.id));
  }
  async function charDelete() {
    const chars = await tavo.character.find('Demo Character');
    if (!chars.length) return tavo.utils.toast('Click tavo.character.create first to create a character');
    const id = await tavo.character.delete(chars[0].id);
    if (!id) return;
    tavo.utils.toast('Deleted Demo Character');
    log('Deleted');
  }
</script>

🎭 Personas

You can use this interface to manage user personas. All persona interfaces are tavo.persona.<method>(...).

Get All Persona Summaries

await tavo.persona.all()

Returns an array of persona summary objects (each item includes id and name):

let personas = await tavo.persona.all()
console.log(personas[0].id)    // e.g. 5
console.log(personas[0].name)  // e.g. "Default User Persona"

Get a Single Persona

await tavo.persona.get(<personaId>)

Gets a persona object by persona ID. Returns null if not found:

let persona = await tavo.persona.get(5)
if (persona) {
  console.log(persona.name)
  console.log(persona.description)
}

Find Personas by Name

await tavo.persona.find(<name>[, <options>])

Finds personas by name and returns an array of persona objects. options.match supports: 'exact' | 'prefix' | 'suffix' | 'contains' (default: 'exact').

let personas = await tavo.persona.find('Default')
let personas2 = await tavo.persona.find('Def', { match: 'prefix' })
console.log(personas.length)

Create a Persona

await tavo.persona.create(<persona>)

Creates a persona and returns the new persona ID. persona.name and persona.description are required.

let id = await tavo.persona.create({
  name: 'Detective Persona',
  description: 'Detail-oriented and strong at structured reasoning.',
  avatar: 'chara/persona-detective.png',
})

Update a Persona

await tavo.persona.update(<persona>)

Updates a persona. persona.id, persona.name, and persona.description are required.

await tavo.persona.update({
  id: 5,
  name: 'Default User Persona',
  description: 'Use a more concise tone and prioritize actionable conclusions.',
  avatar: 'chara/persona-default.png',
  active: true,
})

Delete a Persona

await tavo.persona.delete(<personaId>)

Deletes a persona by persona ID:

await tavo.persona.delete(5)
await tavo.persona.delete(persona)  // persona must be a persona object that includes id

Persona Object Fields

Persona objects (returned by get) commonly include:

{
  id: 5,  // Unique persona ID
  name: 'Default User Persona',  // Persona name (required)
  description: '...',  // Persona description (required)
  avatar: 'xxx.png',  // Persona avatar URL or path (optional)
  active: true,  // Whether this is the default persona
  sortIndex: 12,  // Sorting index
}

Example

Copy the following into any bubble to see it in action.

<h3>Persona API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 100px; max-height: 400px; overflow-y: auto;"></pre>
<div style="display: grid; grid-template-columns: auto auto auto auto auto auto;">
  <button onclick="personaAll()">tavo.persona.all</button>
  <button onclick="personaGet()">tavo.persona.get</button>
  <button onclick="personaFind()">tavo.persona.find</button>
  <button onclick="personaCreate()">tavo.persona.create</button>
  <button onclick="personaUpdate()">tavo.persona.update</button>
  <button onclick="personaDelete()">tavo.persona.delete</button>
</div>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };
  async function personaAll() {
    const personas = await tavo.persona.all();
    log(personas);
  }
  async function personaGet() {
    const id = prompt('Enter a persona ID (you can get it from tavo.persona.all())');
    if (!id) return;
    const persona = await tavo.persona.get(Number(id));
    log(persona);
  }
  async function personaFind() {
    const name = prompt('Enter the persona name to search');
    if (!name) return;
    const personas = await tavo.persona.find(name);
    log(personas);
  }
  async function personaCreate() {
    const id = await tavo.persona.create({
      name: 'Demo Persona',
      description: 'This is a test persona created via tavo.persona.create. It is concise and gives direct conclusions.',
    });
    const persona = await tavo.persona.get(id);
    log(persona);
  }
  async function personaUpdate() {
    const personas = await tavo.persona.find('Demo Persona');
    if (!personas.length) return tavo.utils.toast('Click tavo.persona.create first to create a persona');
    const persona = personas[0];
    const newDesc = prompt('Edit persona description:', persona.description);
    if (!newDesc) return;
    persona.description = newDesc;
    await tavo.persona.update(persona);
    log(await tavo.persona.get(persona.id));
  }
  async function personaDelete() {
    const personas = await tavo.persona.find('Demo Persona');
    if (!personas.length) return tavo.utils.toast('Click tavo.persona.create first to create a persona');
    const id = await tavo.persona.delete(personas[0].id);
    if (!id) return;
    tavo.utils.toast('Deleted Demo Persona');
    log('Deleted');
  }
</script>

🎛️ Presets

You can use this interface to manage presets. All preset interfaces are tavo.preset.<method>(...).

Get All Preset Summaries

await tavo.preset.all()

Returns an array of preset summary objects (each item includes id and name):

let presets = await tavo.preset.all()
console.log(presets[0].id)    // e.g. 1
console.log(presets[0].name)  // e.g. "Default"

Get a Single Preset

await tavo.preset.get(<presetId>)

Gets a preset object by preset ID. Returns null if not found:

let preset = await tavo.preset.get(1)
if (preset) {
  console.log(preset.name)
  console.log(preset.entries.length)
  console.log(preset.basicPrompts.chatStart)
}

Find Presets by Name

await tavo.preset.find(<name>[, <options>])

Finds presets by name and returns an array of full preset objects. options.match supports: 'exact' | 'prefix' | 'suffix' | 'contains' (default: 'exact').

let presets = await tavo.preset.find('Default')
let presets2 = await tavo.preset.find('Def', { match: 'prefix' })
console.log(presets.length)

Create a Preset

await tavo.preset.create(<preset>)

Creates a preset and returns the new preset ID. preset.name is required. Other fields are optional; missing parts in preset.basicPrompts and preset.entries will be filled with built-in defaults.

let id = await tavo.preset.create({
  name: 'My Preset',
  basicPrompts: {
    continueNudge: '[Continue your last message without repeating the original content.]',
  },
  entries: [
    {
      identifier: 'abc123',
      name: '🌸 Style Control',
      content: 'Adopt a refined and elegant narrative style, similar to popular high-quality female-oriented works on platforms like Jinjiang and Changpei.',
    },
  ],
})

Update a Preset

await tavo.preset.update(<preset>)

Updates a preset. preset.id is required. The incoming entries will overwrite the existing entries. A typical flow is get → modify → update.

const preset = await tavo.preset.get(33);
preset.entries.find(e => e.identifier == 'main').content = 'Please reply in Chinese to all of {{user}}\'s questions.';
await tavo.preset.update(preset)

Delete a Preset

await tavo.preset.delete(<presetId>)

Deletes a preset by preset ID:

await tavo.preset.delete(1)
await tavo.preset.delete(preset)  // preset must be a preset object that includes id

Preset Object Fields

Preset objects (returned by get / find) include:

{
  id: 1,          // Preset unique ID
  name: 'Default', // Preset name (required)
  basicPrompts: { /* BasicPrompts, see below */ },
  entries: [],    // PresetEntry[] prompt entries list (see below)
}

Basic Prompt Fields (BasicPrompts)

basicPrompts contains various system prompt templates. All fields are optional; missing fields use built-in defaults:

{
  persona: '{{persona}}',        // Format template for the user persona description
  description: '{{description}}', // Format template for the character description
  personality: '{{personality}}', // Format template for the character personality (insert location via {{personality}})
  scenario: '{{scenario}}',      // Format template for the scenario (insert location via {{scenario}})
  exampleMessageStart: '[Example Chat]',  // Example chat start marker
  chatStart: '[Start a new Chat]',        // Chat history start marker
  groupChatStart: '[Start a new group chat. Group members: {{group}}]',  // Group chat start marker
  groupNudge: '[Write the next reply only as {{char}}.]',  // Nudge a specific role in group chat
  continueNudge: '[Continue your last message without repeating its original content.]',  // Nudge the "continue" action
  impersonation: '[Write your next reply from the point of view of {{user}}...]',  // Impersonation prompt for acting as user
  lorebook: '{0}',  // Wrap template for lorebook entries (insert via {0})
}

Prompt Entry Fields (PresetEntry)

Each item in entries has this structure:

{
  // -- Basic info ------------------------------------
  identifier: 'main',   // Unique entry identifier (built-in entries have fixed identifiers; see the table below)
  name: 'Main Prompt',  // Entry display name
  content: '...',       // Prompt body text (no `content` for marker type)
  enabled: true,        // Whether this entry is enabled (takes effect in the active list)
  active: true,         // Whether it is included in the active list (inactive entries are archived and do not participate in prompt building)

  // -- Type -----------------------------------------
  type: 'custom',       // Entry type:
                        //   'builtin' - Built-in prompts (fixed identifier, e.g. main / jailbreak)
                        //   'marker'  - Position marker (no content; marks where other content gets inserted)
                        //   'custom'  - Custom prompt

  // -- Role and injection (configurable for custom)-
  role: 'system',       // Message role: 'system' | 'user' | 'assistant'
  injectionPosition: 'relative',  // Injection position:
                                  //   'relative' - relative position (follows preset entry order)
                                  //   'absolute' - absolute position (insert at a specific depth in chat history)
  injectionDepth: 4,   // Injection depth (only effective when injectionPosition is 'absolute')
                        // 0 = after the last message, 1 = before the last message, and so on
}

Built-in Entry Identifier Table

The following identifier values correspond to built-in fixed prompts or position markers. You can reference them directly when creating/updating:

identifier Name Type Description
main Main Prompt builtin The main prompt; the core instruction for the conversation
worldInfoBefore Lorebook Before marker Lorebook (insert above character description)
personaDescription Persona Description marker User persona description insert point
charDescription Char Description marker Character description insert point
charPersonality Char Personality marker Character personality insert point
scenario Scenario marker Scenario description insert point
enhanceDefinitions Enhance Definitions builtin Extra prompt to enhance character definitions
nsfw Auxiliary Prompt builtin Auxiliary prompt (defaults to empty)
worldInfoAfter Lorebook After marker Lorebook (insert below character description)
dialogueExamples Chat Examples marker Example dialog insert point
chatHistory Chat History marker Chat history insert point
jailbreak Post-History Instructions builtin Extra instructions after chat history

📚 Lorebooks

You can use this interface to manage lorebooks. All lorebook interfaces are tavo.lorebook.<method>(...).

Get All Lorebook Summaries

await tavo.lorebook.all()

Returns an array of lorebook summary objects (each item includes id, name, and entries):

let lorebooks = await tavo.lorebook.all()
console.log(lorebooks[0].id)       // e.g. 3
console.log(lorebooks[0].name)     // e.g. "City Setting"
console.log(lorebooks[0].entries)  // e.g. 12 (entry count)

Get a Single Lorebook

await tavo.lorebook.get(<lorebookId>)

Gets a lorebook object by ID. Returns null if not found:

let lorebook = await tavo.lorebook.get(3)
if (lorebook) {
  console.log(lorebook.name)
  console.log(lorebook.entries.length)
}

Find Lorebooks by Name

await tavo.lorebook.find(<name>[, <options>])

Finds lorebooks by name and returns an array of lorebook objects. options.match supports: 'exact' | 'prefix' | 'suffix' | 'contains' (default: 'exact').

let lorebooks = await tavo.lorebook.find('City')
let lorebooks2 = await tavo.lorebook.find('City', { match: 'suffix' })
console.log(lorebooks.length)

Create a Lorebook

await tavo.lorebook.create(<lorebook>)

Creates a lorebook and returns the new lorebook ID. lorebook.name is required.

let id = await tavo.lorebook.create({
  name: 'City Setting',
  entries: [],
})

Update a Lorebook

await tavo.lorebook.update(<lorebook>)

Updates a lorebook. lorebook.id and lorebook.name are required.

await tavo.lorebook.update({
  id: 3,
  name: 'City Setting (Remastered)',
  entries: [],
})

Delete a Lorebook

await tavo.lorebook.delete(<lorebookId>)

Deletes a lorebook by ID:

await tavo.lorebook.delete(3)
await tavo.lorebook.delete(lorebook)  // lorebook must be a lorebook object that includes id

Lorebook Object Fields

Lorebook objects (returned by get / find) include:

{
  id: 3,           // Unique lorebook ID
  name: 'City Setting', // Lorebook name (required)
  entries: [],     // LorebookEntry[] entry list (see below)
}

Entry Object Fields (LorebookEntry)

Each item in the entries array has this structure:

{
  // -- Basic info ------------------------------------
  identifier: 'entry-uuid',  // Unique entry identifier (string)
  name: 'City Overview',            // Entry name (for display/search only)
  content: 'This is a coastal city that often has thick fog at night.',  // Main body injected into prompts
  enabled: true,             // Whether this entry is enabled
  strategy: 'constant',      // Trigger strategy: 'constant' (always on) | 'keyword' (keyword-triggered)

  // -- Keywords --------------------------------------
  keywords: ['city', 'port'],         // Primary keyword list (effective when strategy is 'keyword')
  secondaryKeywords: ['night', 'fog'],  // Secondary keyword list
  secondaryKeywordStrategy: 'none',  // Secondary keyword matching strategy:
                                     //   'none'   - Disable secondary keywords
                                     //   'andAny' - Primary matched and any secondary matched (default)
                                     //   'andAll' - Primary matched and all secondary matched
                                     //   'notAny' - Primary matched and none of secondary matched
                                     //   'notAll' - Primary matched and not all secondary matched
  scanDepth: 2,              // Message depth for keyword scanning (default 2, max 1000)
  caseSensitive: false,      // Whether keyword matching is case-sensitive
  matchWholeWord: true,      // Whether to match whole words only

  // -- Injection position ----------------------------
  injectionPosition: 'lorebookBefore',  // Injection position:
                                        //   'lorebookBefore'         - Above character description (↑Char)
                                        //   'lorebookAfter'          - Below character description (↓Char)
                                        //   'topOfExampleMessages'   - Before example dialog
                                        //   'bottomOfExampleMessages'- After example dialog
                                        //   'atDepth'                - Absolute depth in chat history
  injectionDepth: 4,         // Injection depth, only effective when injectionPosition is 'atDepth'
  injectionRole: 'system',   // Injection role: 'system' | 'user' | 'assistant'

  // -- Probability and behavior ----------------------
  probability: 100,  // Activation probability (0-100, default 100)
  sticky: 0,         // Message turns to keep active after activation (0 means no persistence)
  cooldown: 0,       // Cooldown turns after one activation (0 means no cooldown)
  delay: 0,          // Delayed activation turns (0 means immediate)
}

Example

Copy the following into any bubble to see it in action.

<h3>Lorebook API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 100px; max-height: 400px; overflow-y: auto;"></pre>
<div style="display: grid; grid-template-columns: auto auto auto;">
  <button onclick="lorebookAll()">tavo.lorebook.all</button>
  <button onclick="lorebookGet()">tavo.lorebook.get</button>
  <button onclick="lorebookFind()">tavo.lorebook.find</button>
  <button onclick="lorebookCreate()">tavo.lorebook.create</button>
  <button onclick="lorebookUpdate()">tavo.lorebook.update</button>
  <button onclick="lorebookDelete()">tavo.lorebook.delete</button>
</div>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };
  async function lorebookAll() {
    const lorebooks = await tavo.lorebook.all();
    log(lorebooks);
  }
  async function lorebookGet() {
    const id = prompt('Enter a lorebook ID (you can get it from tavo.lorebook.all())')
    if (!id) return;
    const lorebook = await tavo.lorebook.get(id);
    log(lorebook);
  }
  async function lorebookFind() {
    const name = prompt('Enter the lorebook name to search')
    if (!name) return;
    const lorebooks = await tavo.lorebook.find(name);
    log(lorebooks);
  }
  async function lorebookCreate() {
    const lorebook = { name: 'Demo Lorebook', entries: [] };
    lorebook.entries.push({
      identifier: 'abc123',
      name: 'About this city',
      content: 'This is a coastal city that often has thick fog at night.',
      enabled: true,
      strategy: 'constant',
    });
    const id = await tavo.lorebook.create(lorebook);
    const lb = await tavo.lorebook.get(id);
    log(lb)
    const c = confirm('Set it as the lorebook for the current chat?');
    if (!c) return;
    const chat = await tavo.chat.current();
    chat.lorebooks.push(lb);
    tavo.chat.update(chat);
  }
  async function lorebookUpdate() {
    const lorebooks = await tavo.lorebook.find('Demo Lorebook');
    if (!lorebooks.length) return tavo.utils.toast('Click tavo.lorebook.create first to create a lorebook');
    const lorebook = lorebooks[0];
    const newContent = prompt(`Edit the content of the first entry in lorebook ${lorebook.name}:`, lorebook.entries[0].content);
    lorebook.entries[0].content = newContent;
    await tavo.lorebook.update(lorebook);
    const lb = await tavo.lorebook.get(lorebook.id);
    log(lb)
  }
  async function lorebookDelete() {
    const lorebooks = await tavo.lorebook.find('Demo Lorebook');
    if (!lorebooks.length) return tavo.utils.toast('Click tavo.lorebook.create first to create a lorebook');
    const lorebook = lorebooks[0];
    const id = await tavo.lorebook.delete(lorebook);
    log(id)
  }
</script>

🎨 Regex

You can use this interface to manage regex groups (a set of find/replace rules). All regex interfaces are tavo.regex.<method>(...).

Get All Regex (Summaries)

await tavo.regex.all()

Returns an array of regex summary objects (each item includes id, name, and entries; entries is the number of rules, not the rule entries array):

let list = await tavo.regex.all()
console.log(list[0].id)       // e.g. 2
console.log(list[0].name)     // e.g. "My Regex"
console.log(list[0].entries)  // e.g. 5 (number of rules)

Get a Single Regex

await tavo.regex.get(<regexId>)

Gets a regex object by ID. Returns null if not found:

let r = await tavo.regex.get(2)
if (r) {
  console.log(r.name)
  console.log(r.entries.length)
}

Find Regex by Name

await tavo.regex.find(<name>[, <options>])

Finds regex groups by name and returns an array of full regex objects. options.match supports: 'exact' | 'prefix' | 'suffix' | 'contains' (default: 'exact').

let found = await tavo.regex.find('My')
let found2 = await tavo.regex.find('My', { match: 'contains' })
console.log(found.length)

Create a Regex Group

await tavo.regex.create(<regex>)

Creates a regex group and returns the new ID. regex.name is required; regex.entries can be omitted (treated as an empty list). A confirmation dialog will be shown before creating/updating.

let id = await tavo.regex.create({
  name: 'Demo Regex',
  entries: [
    {
      name: 'Status Bar',
      findRegex: '/<status>(.*?)<\/status>/gim',
      replaceString: '<pre>$1</pre>',
      placements: ['char'],
      timing: 'display',
    },
  ],
})

Update a Regex Group

await tavo.regex.update(<regex>)

Updates a regex group. regex.id and regex.name are required (validated by the front-end wrapper). Typical flow: get → modify → update.

const r = await tavo.regex.get(2)
r.entries[0].enabled = false
await tavo.regex.update(r)

Delete a Regex Group

await tavo.regex.delete(<regexId>)

Deletes a regex group by ID. You can also pass a regex object that includes id:

await tavo.regex.delete(2)
await tavo.regex.delete({ id: 2 })

Regex Object Fields

Regex objects (returned by get / find) have this structure:

{
  id: 2,
  name: 'My Regex',
  entries: [ /* RegexEntry[]; see below */ ],
}

Rule Entry Fields (RegexEntry)

Each item in entries:

{
  name: 'Rule display name',             // Required (string); otherwise parsing may fail
  findRegex: 'pattern',                 // Find regex (supports JavaScript-like `/pattern/flags`)
  replaceString: '',                   // Replacement string
  trimStrings: [],                     // Additional strings to trim
  placements: ['char'],               // Target placements (can be multiple):
                                 //   'user'      - user input
                                 //   'char'      - AI output
                                 //   'reasoning' - reasoning content
                                 //   'lorebook'  - lorebook injected content
  timing: 'display',                  // Execution timing:
                                 //   'display'         - only display (do not write into persistent messages; similar to ST markdownOnly)
                                 //   'send'            - only send into the model (before generation)
                                 //   'sendAndDisplay'  - both display and send
                                 //   'receive'         - persist after receiving the reply (relevant for input/output)
                                 //   'editAndReceive'  - persist and rewrite when receiving edited messages
  substitution: 'none',              // Macro substitution mode: 'none' | 'raw' | 'escaped'
  minDepth: null,                    // Optional: message depth lower bound (integer)
  maxDepth: null,                    // Optional: message depth upper bound (integer)
  enabled: true,                     // Whether this rule is enabled
}

If you omit fields, the side-end will fill reasonable defaults for findRegex, replaceString, trimStrings, placements, timing, substitution, enabled, etc. (e.g. placements: ['char'], timing: 'display').


🧠 Long-Term Memory

You can use this interface to read or modify long-term memory for the current chat. All interfaces are tavo.memory.<method>(...).

Get Current Memory

await tavo.memory.current()

Gets the current chat memory object:

const memory = await tavo.memory.current()
console.log(memory.enabled)         // true / false
console.log(memory.memories.length) // Number of memory items

Update Memory

await tavo.memory.update(<memory>)

Updates current chat memory and returns the updated object. Updatable fields:

  • enabled: whether memory is enabled
  • memories: memory text array (string[])
const memory = await tavo.memory.current()

memory.enabled = true
memory.memories = [
  'The user prefers concise answers with conclusions first',
  'The user tends to keep the character calm and professional',
]

const updated = await tavo.memory.update(memory)
console.log(updated)

Memory Object Fields

current / update returns this object shape:

{
  id: 12,  // Memory record ID
  enabled: true,  // Whether long-term memory is enabled
  memories: [     // Memory item list (string array)
    'User prefers concise replies',
    'Avoid repeating already-confirmed information'
  ],
}

Example

Copy the following into a bubble to see it in action:

<h3>Long-Term Memory API Demo</h3>
<button onclick="addOneMemory()">Add one memory</button>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 80px; max-height: 300px; overflow-y: auto;"></pre>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };

  async function addOneMemory() {
    const memory = await tavo.memory.current();
    if (!memory.enabled) {
      memory.enabled = true;
      tavo.utils.toast('Long-term memory enabled automatically')
    }
    memory.memories.push(`Watched a grand fireworks show with Guoguo ${new Date().toISOString()}`);
    const id = await tavo.memory.update(memory);
    tavo.utils.toast(`Added 1 memory item, now total ${memory.memories.length}`);
    log(memory);
  }
</script>

Generation Requests

You can use this interface to trigger a one-off text generation directly. All generation requests use tavo.generate(...).

Start Generation

await tavo.generate(<prompt>, <options>)

  • prompt type string: user input for this generation
  • options type object: generation options (pass {} if no extra config is needed)

The return type is string, i.e. text generated by the model.

const result = await tavo.generate('Summarize what happened today in one sentence', {})
console.log(result)

options Fields

options supports the following fields:

  • context type boolean (default false):
    1. true: generate with current conversation context (reuses current chat state)
    2. false: AI generation request unrelated to current conversation (default)
  • preset type number | object (optional):
    1. Pass a preset ID directly, e.g. 12
    2. If passing an object, only id is recognized, e.g. { id: 12 }
  • settings type object (optional): override model parameters for this request

Example:

const text = await tavo.generate(
  'Based on our recent conversation, give me 3 action suggestions',
  {
    context: true,
    preset: { id: 8 },
    settings: {
      temperature: 0.7,
      topP: 0.9,
      maxCompletionTokens: 300,
    },
  },
)

console.log(text)

Notes

  • This interface is a one-off request that returns full text, not streaming chunks
  • Generation requests use the model endpoint bound to the current chat; if no endpoint is available for the current chat, it returns null

Example

Copy the following into a bubble to see it in action:

<h3>Generation Request API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 80px; max-height: 300px; overflow-y: auto;"></pre>
<button id="btn-generate" onclick="generate()">Generate Character Card</button>
<p id="status"></p>
<div id="actions" style="display:none; gap:8px;">
  <button onclick="downloadJson()">Download JSON File</button>
  <button onclick="createCharacter()">Create Character Card Directly</button>
</div>
<script>
let generatedCard = null;
const log = (...args) => {
  const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
  document.getElementById('log').textContent = text + '\n\n';
};
function setUi(loading, status, showActions = false) {
  document.getElementById('btn-generate').disabled = loading;
  document.getElementById('status').textContent = status;
  document.getElementById('actions').style.display = showActions ? 'flex' : 'none';
}
async function generate() {
  const p = prompt('Enter the character traits you want');
  if (!p) return;
  setUi(true, 'Generating...');
  try {
    let text = await tavo.generate(`Generate a character card from the following info. Output JSON that follows Character Card Spec V3.\n${p}`);
    log(text)
    text = text.trim();
    if (text.startsWith('```') && text.endsWith('```')) {
      text = text.replace(/^```[a-zA-Z]*\n?/, '').replace(/```$/, '');
    }
    generatedCard = JSON.parse(text);
    if (generatedCard.mes_example instanceof Array) generatedCard.mes_example = generatedCard.mes_example.join('\n')
    setUi(false, `Character card "${generatedCard.name}" generated`, true);
  } catch (e) {
    log(e);
    console.log(e);
    setUi(false, `Character card generation failed`, false)
  }
}
function downloadJson() {
  tavo.utils.export(`${generatedCard.name}.json`, JSON.stringify(generatedCard));
}
async function createCharacter() {
  await tavo.character.create(generatedCard);
}
</script>

⌨️ Input Box

You can use this interface to read or manipulate the chat input box. All input interfaces are tavo.input.<method>(...).

Read Input Box

await tavo.input.get()

Gets the current text content in the input box:

let text = await tavo.input.get()  // Get current input box content

Write to Input Box

tavo.input.set(<text>)

Overwrites the input box content (clears existing content):

tavo.input.set('Hello!')  // Replace input box content with "Hello!"

Append to Input Box

tavo.input.append(<text>)

Appends text to the end of current input box content:

tavo.input.append(' Let us keep chatting')  // Append text to existing content

Clear Input Box

tavo.input.clear()

Clears input box content:

tavo.input.clear()

Send Message

tavo.input.send()

Triggers sending the current input box content:

tavo.input.set('Nice weather today')
tavo.input.send()  // Send automatically

Example

Copy the following into any bubble to see it in action.

<h3>Input Box API Demo</h3>
<pre id="log" style="background: #0006; font-size: 12px; padding: 1em 1.5em; min-height: 60px; max-height: 200px; overflow-y: auto;"></pre>
<div style="display: grid; grid-template-columns: auto auto auto auto auto;">
  <button onclick="inputGet()">tavo.input.get</button>
  <button onclick="inputSet()">tavo.input.set</button>
  <button onclick="inputAppend()">tavo.input.append</button>
  <button onclick="inputClear()">tavo.input.clear</button>
  <button onclick="inputSend()">tavo.input.send</button>
</div>
<script>
  const log = (...args) => {
    const text = args.map(v => typeof v === 'string' ? v : JSON.stringify(v, null, 2)).join(' ');
    document.getElementById('log').textContent = text + '\n\n';
  };
  async function inputGet() {
    const text = await tavo.input.get();
    log('Current input content:', JSON.stringify(text));
  }
  function inputSet() {
    const text = prompt('Enter content to write into the input box', 'Hello!');
    if (text === null) return;
    tavo.input.set(text);
    tavo.utils.toast('Written to input box');
  }
  function inputAppend() {
    const text = prompt('Enter content to append to the end of the input box', '(appended content)');
    if (text === null) return;
    tavo.input.append(text);
    tavo.utils.toast('Appended to input box');
  }
  function inputClear() {
    tavo.input.clear();
    tavo.utils.toast('Input box cleared');
  }
  function inputSend() {
    tavo.input.set('This message is automatically sent by tavo.input.send()');
    tavo.input.send();
  }
</script>

🛠️ Utilities

General utility interfaces. All utility interfaces are tavo.utils.<method>(...).

Toast

tavo.utils.toast(<text>)

Shows a lightweight toast notification that disappears automatically after a few seconds.

Open URL

tavo.utils.openUrl(<url>)

Opens a URL in an external browser:

tavo.utils.openUrl('https://example.com')

Export File

tavo.utils.export(<name>, <data>)

Exports data as a file and triggers system share/save. data can be Base64-encoded content (recommended) or plain text:

tavo.utils.export('Yeli Character Card', btoa('This is text or binary data converted to base64 using btoa'))  // Pass base64 data (recommended)
tavo.utils.export('record.txt', 'This is plain text content')  // Plain text

Example

Copy the following into any bubble to see it in action.

<h3>Utilities API Demo</h3>
<div style="display: grid; grid-template-columns: auto auto auto; gap: 8px;">
  <button onclick="utilsToast()">tavo.utils.toast</button>
  <button onclick="utilsOpenUrl()">tavo.utils.openUrl</button>
  <button onclick="utilsExport()">tavo.utils.export</button>
</div>
<script>
  function utilsToast() {
    const text = prompt('Enter toast text to display', 'Hello, this is a Toast notification!');
    if (!text) return;
    tavo.utils.toast(text);
  }
  function utilsOpenUrl() {
    const url = prompt('Enter a URL to open', 'https://example.com');
    if (!url) return;
    tavo.utils.openUrl(url);
  }
  function utilsExport() {
    const filename = prompt('Enter export filename', 'demo.txt');
    if (!filename) return;
    const content = `This is a test file exported by tavo.utils.export.\nExport time: ${new Date().toLocaleString()}`;
    tavo.utils.export(filename, content);
  }
</script>

📱 App

You can use this interface to read some app properties. All interfaces are tavo.app.<method>(...).

Get Current App Version

await tavo.app.version();  // string: 0.77.0
await tavo.app.versionNumber();  // number: 770