Files
NoteDiscovery/documentation/PLUGINS.md
T
Gamosoft e6b72d6d89 Add a contrib folder for community plugins
The loader only globs plugins/*.py, so plugins/contrib/ ships with the
project without being loaded - users opt in by copying a file up one
level. Each plugin documents itself in its docstring, so contributing one
touches no shared file and can't conflict with another.
2026-08-10 13:49:54 +02:00

6.9 KiB

🔌 Plugin System

NoteDiscovery includes a powerful plugin system that lets you extend functionality without modifying core code.

How Plugins Work

Plugins are Python files that live in the plugins/ directory. They use event hooks to react to actions in the app:

Available Hooks

Hook When Triggered Parameters Can Modify
on_note_create New note is created note_path, initial_content Yes (return modified content)
on_note_save Note is being saved note_path, content Yes (return transformed content, or None)
on_note_load Note is loaded from disk note_path, content Yes (return transformed content, or None)
on_note_delete Note is deleted note_path No
on_search Search is performed query, results Yes (return a replacement result list, or None)
on_app_startup App starts up None No

Every hook that can modify follows the same rule: return a new value to replace the one you were given, or return None to leave it alone. Plugins run in filename order, each receiving what the previous one left behind. A plugin that raises is logged and skipped — the value keeps its last good state, so one broken plugin can't take down a request.

Replacements must be the right type (str for content hooks, list for on_search). Anything else is logged and ignored rather than passed on.

Search result ordering

Core search results are re-sorted by path before pagination, so that paging through them stays reproducible while notes are being edited. If your on_search returns its own list, that sort is skipped and your order is preserved — which also makes the order your responsibility. Return results in a stable order (due date, name, relevance score), not one that shifts between calls, or paginated clients can see duplicates.

Bundled and Contributed Plugins

note_stats ships in plugins/ and loads at startup. Community plugins live in plugins/contrib/, which the loader ignores — install one by copying it into plugins/ and restarting. Each carries its own documentation in its docstring; see plugins/contrib/README.md.

Creating a Plugin

1. Create a Python file

cd notediscovery/plugins
touch my_plugin.py

2. Define your plugin class

Every plugin must have a Plugin class with:

  • name - Display name
  • version - Version string
  • enabled - Whether it's active (default: True)

3. Implement event hooks

Add methods for the events you want to handle. Method names are checked at load time: an on_* method that isn't a known hook gets a warning in the log rather than silently never running.

Plugin Context

If your plugin defines setup(ctx), it's called once at startup before any hook fires. Use it instead of trying to work out where things live yourself — the context always agrees with the running app, including NOTES_DIR overrides.

Field What it is
ctx.notes_dir Path to the vault the app is actually serving
ctx.plugins_dir Path to the plugins directory
ctx.config The resolved app config dict
ctx.logger Logger named for your plugin, output goes to the server log
class Plugin:
    def __init__(self):
        self.name = "My Plugin"
        self.version = "1.0.0"
        self.enabled = True

    def setup(self, ctx):
        self.notes_dir = ctx.notes_dir
        self.log = ctx.logger
        self.log.info("ready, watching %s", self.notes_dir)

Plugin Routes

A plugin can serve its own HTTP endpoints by returning an APIRouter from get_routes(). Routes are mounted under /api/plugins/<plugin_id>/ and inherit the app's authentication.

from fastapi import APIRouter

class Plugin:
    # ... name / version / enabled as above ...

    def get_routes(self) -> APIRouter:
        router = APIRouter()

        @router.get("/summary")
        async def summary():
            if not self.enabled:
                return {"enabled": False}
            return {"enabled": True, "notes": 42}

        return router

That endpoint is then reachable at /api/plugins/my_plugin/summary. Routes are mounted at startup whether or not the plugin is enabled, so check self.enabled inside the handler if it should go quiet when toggled off.

The bundled note_stats plugin uses both of these — see plugins/note_stats.py for a working example.

Basic Example: Note Logger

This simple plugin logs note activity to Docker logs (visible with docker-compose logs -f):

"""
Note Logger Plugin
Logs all note operations to Docker logs for monitoring
"""

class Plugin:
    def __init__(self):
        self.name = "Note Logger"
        self.version = "1.0.0"
        self.enabled = True
    
    def on_note_save(self, note_path: str, content: str) -> str | None:
        """Log when a note is saved"""
        word_count = len(content.split())
        print(f"💾 Note saved: {note_path} ({word_count} words)")
        return None  # Don't modify content, just observe
    
    def on_note_delete(self, note_path: str):
        """Log when a note is deleted"""
        print(f"🗑️  Note deleted: {note_path}")
    
    def on_search(self, query: str, results: list) -> list | None:
        """Log search queries"""
        print(f"🔍 Search: '{query}' → {len(results)} results")
        return None  # Don't touch the results, just observe

How to see the logs

# View logs in real-time
docker-compose logs -f

# View logs for specific service
docker-compose logs -f notediscovery

Activating Your Plugin

  1. Place the file in plugins/ directory
  2. Restart the app: docker-compose restart
  3. Plugin auto-loads: Plugins with enabled = True will automatically load

Enable/Disable Plugins via API

Use the API to toggle plugins on/off:

Linux/Mac:

# Enable a plugin
curl -X POST http://localhost:8000/api/plugins/note_logger/toggle \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'

# Disable a plugin
curl -X POST http://localhost:8000/api/plugins/note_logger/toggle \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

Windows PowerShell:

# Enable a plugin
curl.exe -X POST http://localhost:8000/api/plugins/note_logger/toggle -H "Content-Type: application/json" -d "{\"enabled\": true}"

# Disable a plugin
curl.exe -X POST http://localhost:8000/api/plugins/note_logger/toggle -H "Content-Type: application/json" -d "{\"enabled\": false}"

List all plugins (all platforms):

curl http://localhost:8000/api/plugins

Plugin State Persistence

Plugin states (enabled/disabled) are saved in plugins/plugin_config.json and persist between restarts.


💡 Tip: Use print() statements in plugins to log to Docker logs for debugging and monitoring!