Skip to main content

Plugin Author Guide

You are reading the dev branch of the docs, which may describe unreleased changes. Read the main docs.

What Is a Plugin?

A plugin is a self-contained package that extends Sapphire — a folder with a single plugin.json manifest, dropped into user/plugins/ (yours; the built-in ones ship in core/plugins/). It can do far more than add abilities the AI calls; it hooks into parts of the pipeline the LLM never touches:

  • Add tools — functions the AI can call during a conversation (search the web, save a memory, control a device)
  • Intercept voice input after speech-to-text, before the LLM ever sees it (post_stt)
  • Filter or rewrite the AI's response before it's saved to history (post_llm)
  • Inject context into the system prompt every turn (prompt_inject for long-lived state; ghost_inject for cache-friendly per-turn ephemera like time, weather, or mood)
  • Block or modify tool arguments before execution (pre_execute)
  • Control TTS — change the voice, rewrite text, cancel speech, or send her voice to an avatar, a game or a robot instead of these speakers (pre_tts, tts_chunk_audio; voice routing)
  • React to wakeword detection before recording starts (on_wake)
  • Provide custom inference providers — TTS, STT, Embedding, or LLM backends that appear in settings dropdowns (guide)
  • Run scheduled tasks on cron timers, independent of any conversation
  • Run background daemons — listen for external events (messages, emails) and trigger AI responses (guide)
  • Register voice commands — keyword triggers that bypass the LLM entirely
  • Ship a settings UI, a dashboard widget, a full-page app, or a theme

In short, a plugin is an autonomous package that can reshape how Sapphire behaves at every stage — input, processing, output, and beyond. This guide covers building that whole spectrum.

How Plugins Get Made

  • By hand — a developer writes the manifest and code. The full toolkit is documented here in this guide.
  • By a coding agent — point an agent like Claude Code (see the bundled claude-code plugin) at this guide and the codebase, and it can author a complete plugin end to end.
  • By Toolmaker (one slice) — Sapphire's built-in Toolmaker lets the AI create a single tool at runtime. It's deliberately simplified so small local models can use it reliably — it makes one slice (a tool packaged as a minimal plugin), not the full thing. For anything beyond a lone tool — hooks, daemons, providers, UI — you want a real plugin, which is what this guide is for.

Contents

Guide What's Inside
Design Principles Read first: core is minimal, plugin or core, one system, one feature one sentence, what shipping costs — the bar for a plugin that ships with Sapphire
Manifest plugin.json reference — fields, priority bands, directory structure
Hooks All hook points (incl. ghost_inject, streaming-TTS, chat lifecycle), HookEvent fields, system access, the privacy_aware gate for private chats, examples
Voice Routing Sending her voice somewhere else — what each TTS hook carries, the Play speech here switch, consumed chunks, an avatar example
Voice Commands Keyword triggers that bypass the LLM — match modes, handlers, macros
Tools Tool file format, schema flags, scopes, reading settings, plugin + chat-scoped state, privacy patterns
Routes Custom HTTP endpoints — path params, auth enforcement, handler signature
Schedule Cron tasks — manifest fields, handler contract, examples
Daemons Background event listeners — Telegram, Discord, Email, custom sources
Subprocesses Running & supervising external programs — ProcessManager lifecycle, process-group cleanup, advanced async helper
Providers Custom TTS, STT, Embedding, LLM backends — base classes, manifest, lifecycle
Memory Layers Plugins adding layers to the Mind Palace
Thinking Blocks core.think — the one reader of think tags: strip / split / wrap, the four rules, the web twin
Prompt Packs Shipping prompts with a plugin
Widgets Dashboard panels — manifest, render contract, settings schema, sample plugin
Games Game Room games — engine contract, sealed seat, board modules, sessions-are-chats
Devices Device drivers - teach Sapphire a machine or gadget: manifest declaration, the three driver functions, secrets, the tool door
Agents Agent kinds - a background worker she spawns: the Agent class, ask/event/report, privacy flags, the four core tools
Stories Story packs — rooms, referee, dice, sealed blanks, identity modes, backdrops
Apps Full-page plugin UIs in the Apps nav — render/cleanup contract, navrail + flyout promotion
Settings Manifest-declared settings, custom web UI, settings API, danger confirms
Web UI Shared JS modules, CSS variables, modals, CSRF, style injection
Themes Custom themes — CSS, animated JS backgrounds, per-theme settings (Settings > Visual)
Signing Verification states, sideloading, signing your own plugins
Lifecycle Startup, live toggle, hot reload, rescan, error isolation, chat lifecycle
Publishing How to structure your repo and submit to the Sapphire Store
Examples Minimal working examples of every capability type — copy-paste templates
AI Reference Compact reference for Sapphire's own use when building plugins

Quick Start

Minimal plugin — logs every chat:

user/plugins/my-plugin/
  plugin.json
  hooks/greet.py

plugin.json:

{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "Logs every chat",
  "author": "you",
  "capabilities": {
    "hooks": {
      "post_chat": "hooks/greet.py"
    }
  }
}

hooks/greet.py:

import logging
logger = logging.getLogger(__name__)

def post_chat(event):
    logger.info(f"User: {event.input}")
    logger.info(f"AI: {event.response}")

Enable in Settings > Plugins. It loads immediately — no restart.


Where Plugins Live

Path Band Priority Range Tracked
core/plugins/ System 0-99 Yes
user/plugins/ User 100-199 No (gitignored)

User plugin priorities are automatically offset into the 100-199 range. Your own plugins always go in user/plugins/. A plugins/ folder at the repo root is not scanned. Both bands import as plugins.<name>, whichever folder they sit in.


Plugin Tests

Tests for a plugin live alongside it, at <plugin>/tests/test_*.py — core/plugins/<name>/tests/ for shipped plugins, user/plugins/<name>/tests/ for yours. They travel with the plugin on install/uninstall so authors can maintain them next to the code they cover.

user/plugins/my-plugin/
  plugin.json
  tools/my_tool.py
  tests/
    test_my_tool.py        # no __init__.py

Shipped-plugin tests run from the repo root with pytest — the root pytest.ini sets testpaths = tests core/plugins and skips user/, infra/, and tmp/. Run your own with python -m pytest user/plugins/my-plugin/tests from the repo root; from plugins.my_plugin import ... resolves there too, because the plugins.<name> namespace spans both bands.

Do not add an __init__.py to tests/. pytest collects tests across every plugin's tests/ dir simultaneously; an __init__.py turns each one into the same import name (tests) and triggers import-name collisions. Leave the directory as plain, non-package — pytest's rootdir-relative collection handles it fine.


Complete Example

A plugin combining hooks, tools, voice commands, and a scheduled task:

user/plugins/smart-home/
  plugin.json
  hooks/context.py
  hooks/quick_lights.py
  tools/devices.py
  routes/camera.py
  schedule/lock_check.py
  web/
    index.js
{
  "name": "smart-home",
  "version": "2.0.0",
  "description": "Full smart home integration",
  "author": "you",
  "url": "https://example.com",
  "priority": 50,
  "capabilities": {
    "hooks": {
      "prompt_inject": "hooks/context.py"
    },
    "voice_commands": [
      {
        "triggers": ["lights on", "lights off"],
        "match": "exact",
        "bypass_llm": true,
        "handler": "hooks/quick_lights.py"
      }
    ],
    "tools": ["tools/devices.py"],
    "routes": [
      {
        "method": "POST",
        "path": "camera/{room}",
        "handler": "routes/camera.py:handle_snapshot"
      }
    ],
    "schedule": [
      {
        "name": "Nightly Lock Check",
        "cron": "0 23 * * *",
        "handler": "schedule/lock_check.py"
      }
    ],
    "web": { "settingsUI": "plugin" }
  }
}

See each guide above for the details on every capability.

Edit this page on GitHub · docs/plugin-author/README.md @ a5fa790 · synced