T.H.E.R.O.N.
← Back API Plugins
GUI · desktop appCLI · headlessServerPlugins · OverviewPlugins · Build

Inhalt

Überblick Installieren & verwalten Sicherheit & Signaturen Ein Plugin bauen plugin.toml Die Plugin-API (ctx) Prozess-Nodes Seiten & Einstellungen Hooks: in Abläufe eingreifen Anbindungen & Sync Werkzeuge für Agents Packen & signieren Fehlersuche

Plugins · Überblick & Referenz

Auf dieser Seite: was Plugins können, wie du sie installierst, aktualisierst und verwaltet (auch die offiziellen Plugins), was Signaturen und Berechtigungen bedeuten – und zum Nachschlagen die Referenz: plugin.toml, alle ctx-Aufrufe, Nodes, Hooks, Packen & Signieren.
Du willst selbst ein Plugin bauen? Dann fang mit Plugins · Entwickeln an – dem Handbuch mit Schritt-für-Schritt-Beispielen – und schlag hier nach.

Überblick

Plugins erweitern Theron um das, was dir fehlt – ohne auf ein neues Release zu warten. Ein Plugin ist ein Python-Paket mit einer plugin.toml, gepackt als .theronplugin-Datei.

  • Neue Navigationspunkte mit eigener Oberfläche (z. B. ein Trello-Board in Theron).
  • Zusätzliche Einstellungen – aus der plugin.toml erzeugt, auf Wunsch mit eigener Oberfläche.
  • Neue Nodes für Prozesse – im Editor wie die eingebauten, mit Feldern, ✔/✖- oder benannten Ausgängen (z. B. ein Switch), mehreren Eingängen und Unterstützung durch den KI-Designer.
  • Anbindungen – z. B. Task-Listen mit Trello, Jira oder GitHub Issues synchronisieren: Zugriff auf Task-Listen, Ereignisse, Zeitpläne und HTTP.
  • Werkzeuge für Agents – zusätzliche Tools, die jeder Claude-Agent während seiner Arbeit nutzen kann.
  • Hooks – in Therons Abläufe eingreifen und das Ergebnis verändern: Prompts und Ergebnisse von Agents, Rückfragen (oder ihre Antwort), KI-Entwürfe, Task-Listen, das Team des THERON-Assistenten.
  • Prozesse & Agents lesen, anlegen, ändern und löschen – und selbst mit Claude sprechen: einzelne Antworten, Gespräche, eigene Claude-Code-Agents.

Eine Vorlage mit Beispielen für alles davon – inklusive einer Trello-Synchronisierung – liegt öffentlich auf GitHub: github.com/mibexx/theron-plugin-sekeleton.

Fertige Plugins zum Herunterladen – mit Suche und Beschreibung – findest du im Plugin-Katalog. Schritt für Schritt erklärt das Plugin-Handbuch, wie du Seiten, Nodes, Fenster, Buttons und Formularfelder hinzufügst, mit Task-Listen arbeitest, auf Events und Transaktionen reagierst und laufende Agents liest.

Installieren & verwalten

  • Offizielle Plugins (ab 0.18): Einstellungen → Plugins → „Offizielle Plugins …“ zeigt alle Plugins von mibexx – direkt vom Plugin-Server plugins.theron.mibexx.de. Ein Klick installiert oder aktualisiert; jede ältere Version lässt sich ebenfalls wählen. Theron nimmt die neueste Version, die zu deiner Theron-Version passt, prüft die Prüfsumme und die offizielle Signatur und zeigt Updates in der Plugin-Liste an (⬆).
  • In der App: Einstellungen → Plugins → „Plugin hochladen …“ und die .theronplugin-Datei wählen. Theron zeigt Name, Version, Signatur und was das Plugin nutzt, bevor du es installierst. Dort schaltest du Plugins aus und ein, öffnest ihre Einstellungen und ihr Protokoll oder entfernst sie.
  • In der Konsole (auch auf einem Slave): theron plugin install DATEI – oder ein offizielles Plugin per ID: theron plugin install elevenlabs, theron plugin update.
  • Im Headless-Worker (Docker): .theronplugin-Dateien nach /data/plugin-packages legen – sie werden bei jedem Start installiert oder aktualisiert. Oder docker exec theron-worker theron plugin install /data/x.theronplugin --yes.
  • Neustart: Neue, geänderte und entfernte Plugins gelten nach einem Neustart von Theron.
  • Wo es läuft: Ein Plugin-Node läuft dort, wo der Prozess läuft. Steuert deine App einen Slave, muss das Plugin auf dem Slave installiert sein. Fehlt es, bleibt der Node im Prozess und Theron fragt nach, wenn der Ablauf dort ankommt.
BefehlWirkung
theron plugin listInstallierte Plugins mit Zustand und Signatur.
theron plugin info DATEI|IDManifest, Berechtigungen, Einstellungen, Signatur.
theron plugin install DATEI [--yes]Installieren oder aktualisieren (.theronplugin, .zip oder Ordner).
theron plugin officialDie offiziellen Plugins: was installiert ist, was ein Update hat.
theron plugin install ID[@VERSION]Ein offizielles Plugin vom Plugin-Server – ohne Version die neueste, die passt.
theron plugin update [ID …]Offizielle Plugins aktualisieren (ohne IDs: alle mit Update).
theron plugin enable|disable IDEin- bzw. ausschalten.
theron plugin remove ID [--purge]Entfernen; --purge löscht auch Einstellungen und Daten.
theron plugin new ORDNEREin minimales Plugin zum Loslegen.
theron plugin pack ORDNER [--sign SCHLÜSSEL] [--out ZIEL]ID-VERSION.theronplugin bauen, optional signiert.
theron plugin keygen DATEIEinen Signaturschlüssel erzeugen.
theron plugin trust ÖFFENTLICHER_SCHLÜSSEL [--name NAME]Plugins mit diesem Schlüssel gelten als verifiziert.

Sicherheit & Signaturen

Plugins laufen innerhalb von Theron und haben dieselben Rechte wie Theron selbst – Python lässt sich nicht abschotten. Die Berechtigungen in der plugin.toml (network, tasklists …) zeigen dir, was ein Plugin nutzt; sie sind ein Hinweis, keine Sperre. Installiere nur Plugins, deren Herkunft du vertraust.

  • ✔ verifiziert: signiert mit einem Schlüssel, dem Theron vertraut – dem Theron-Schlüssel von mibexx oder einem, den du mit theron plugin trust hinzugefügt hast.
  • signiert, Schlüssel unbekannt: unverändert seit dem Signieren, aber von jemandem, den Theron nicht kennt. Der Fingerprint wird angezeigt.
  • nicht signiert: Theron fragt ausdrücklich nach, ob du dem Plugin vertraust.
  • ✖ ungültig: nach dem Signieren verändert – wird nie installiert.

Bricht ein Plugin beim Laden ab, schaltet Theron es mit seiner Fehlermeldung aus; alle anderen laufen weiter. Hat ein Plugin Theron beim Laden zum Absturz gebracht, ist es beim nächsten Start ausgeschaltet. Mit THERON_NO_PLUGINS=1 oder --no-plugins startet Theron ganz ohne Plugins.

Ein Plugin bauen

Am schnellsten mit der Vorlage: git clone https://github.com/mibexx/theron-plugin-sekeleton my-plugin – oder ganz klein mit theron plugin new my-plugin. Aufbau:

my-plugin/
├── plugin.toml          Manifest: ID, Version, Berechtigungen, Einstellungen
├── my_plugin/           dein Python-Paket
│   └── __init__.py      def register(ctx): …
├── lib/                 optional: reine Python-Abhängigkeiten
└── i18n/de.json         optional: Übersetzungen deiner Texte

Theron ruft beim Start register(ctx) auf. Über ctx meldet das Plugin an, was es mitbringt – das ist die einzige Schnittstelle zu Theron und bleibt innerhalb einer API-Version stabil.

def register(ctx):
    async def hello(node):
        node.progress("greeting …")
        return node.success(f"Hello – the input had {len(node.input)} characters.")

    ctx.nodes.register("hello", label=ctx.tr("Say hello"), run=hello, outputs="success")
  • Python: Plugins liegen als Quelltext vor und laufen mit dem Python, das in Theron steckt (3.12 oder neuer). Die ganze Standardbibliothek steht zur Verfügung.
  • Abhängigkeiten: In Theron gibt es kein pip. Reine Python-Bibliotheken legst du nach lib/ (pip install --target lib PAKET); Bibliotheken mit nativem Code gehen nicht. Für HTTP gibt es ctx.http.
  • Importe im Plugin relativ schreiben (from .trello import Client).
  • Texte sind englisch; ctx.tr("…") übersetzt sie mit i18n/<sprache>.json.

plugin.toml

id = "trello-sync"             # Kleinbuchstaben, Ziffern, "-" – eindeutig
name = "Trello Sync"
version = "1.0.0"
description = "Synchronisiert Task-Listen mit einem Trello-Board."
author = "Dein Name"
license = "MIT"
theron_api = 1                 # Plugin-API, für die es geschrieben ist
min_theron = "0.14.0"          # optional
entry = "trello_sync"          # das Python-Paket mit register(ctx)
permissions = ["network", "tasklists"]

[[settings]]
key = "api_key"
type = "secret"                # text, secret, textarea, number, bool, choice, tasklist, project
label = "API key"
required = true

Berechtigungen: network, tasklists, processes, agents, runs, agent-tools, hooks, ui, files, ai. Jede Einstellung erscheint automatisch im Einstellungsdialog des Plugins; tasklist und project werden zur Auswahlliste.

Die Plugin-API (ctx)

ctx.…Wofür
settings.get(key) · settings[key] · settings.set(key, value) · settings.on_change(fn) · settings.add_button(label, run) · settings.on_edit(run, keys)Die Einstellungen des Plugins; Buttons im Einstellungsdialog, z. B. „Testen“ (run(values) → Text oder (ok, text)); on_edit reagiert, während Felder im Dialog geändert werden – prüfen, nachfragen, andere Felder füllen ({"values": {…}, "ok", "text"}, ab 0.20). Auswahl-Optionen dürfen {value, label} sein.
storage.get/set/delete/keys/itemsKleiner dauerhafter Speicher (Schlüssel → JSON), z. B. Trello-Karte ↔ Requirement.
data_dirEigener Ordner für Dateien; bleibt bei Updates erhalten.
nodes.register(kind, label=, run=, fields=, outputs=, inputs=, wait=, description=, ai_hint=, icon=)Ein Prozess-Node – ✔/✖ oder benannte Aus-/Eingänge (fest oder aus den Einstellungen); Auswahlfelder mit Optionen aus einer Funktion, Felder mit show_if.
{"id", "label", "color"} an Aus-/EingängenFarbe des Ports und seiner Kanten, frei vom Plugin gewählt: success (grün), error (rot, gestrichelt), neutral, info oder #rrggbb – auch abhängig von den Einstellungen (ab 0.18).
"buttons": [{"label", "run"}] an einem FeldButtons neben einem Node-Feld, z. B. ▶ Hörprobe: run(config) läuft mit den aktuellen Einstellungen, das Label darf eine Funktion sein (▶ / ■) (ab 0.18).
tools.register(name, description=, params=, run=)Ein Werkzeug für Claude-Agents.
ui.add_page(title, factory, icon=) · ui.settings_page(factory) · ui.call(fn) · ui.open_url(url)Oberfläche (nur Desktop – ui.available).
ui.add_panel(page, title, factory, area=) · ui.open_window(title, factory, id=)Fenster neben/unter jeder Seite (folgen dem, was die Seite zeigt) und eigene Fenster – Handbuch.
ui.add_action(page, label, run, place=) · ui.add_form_button(form, label, run) · fields.add(form, key, …)Buttons in Werkzeugleisten, neben „Senden“, neben „+ Neu …“, auf der Task-Listen-Seite, in der Seitenleiste; Felder und Buttons in den Formularen von Agents, Prozessen, Projekten – Handbuch.
ui.ask_text · ui.ask_choice · ui.message · ui.chat_input · ui.set_chat_input · ui.ask_files · ui.ask_folder · await ui.run(fn)Kleine Dialoge im Theron-Look (ask_text auch mit Vorschlagsliste); Datei- und Ordnerwahl; mit ui.run aus Nodes und Buttons heraus etwas auf dem UI-Thread ausführen und das Ergebnis bekommen (ab 0.20); die Chat-Eingabe lesen und füllen.
chat.add_format(pattern, render) · chat.add_renderer(kind, render) · chat.link(name, text, …) · chat.on_link(name, fn) · chat.refresh()Den Chat erweitern: Text in jeder Nachricht anders darstellen, eigene Blöcke (Player, Dokumente, Diagramme …) mit klickbaren Buttons – das Plugin rendert, Theron bietet die Haken – Handbuch.
await node.ask(question, options) · node.say(text, kind=, data=)Ein Node fragt den Nutzer im Chat und wartet auf die Antwort; ein Node postet in den Chat (Text oder eigener Block).
events.on(name, fn)Auf Ereignisse reagieren (siehe unten).
schedule(seconds, fn, first=) · on_start(fn) · on_stop(fn)Regelmäßige Aufgaben, Start und Ende.
spawn(fn, *args)Etwas im Hintergrund ausführen, z. B. nach einem Klick (gibt ein Future zurück).
tasklists.projects/list/get/find/create/update/delete, add_epic/update_epic/delete_epic, add_requirement/update_requirement/delete_requirementTask-Listen, Epics und Requirements.
processes.list/get/find/create/create_from_draft/to_draft/update/delete · agents.list/get/find/create/update/deleteProzesse und Agents lesen, anlegen, ändern, löschen.
hooks.on(name, fn, timeout=) · await hooks.call(name, data) · events.emit(name, payload)In Abläufe eingreifen; eigene Hooks und Ereignisse anbieten.
runs.start(project_id, text, title=) · runs.get/list · runs.messageTasks starten und lesen.
runs.active/state/workers/events/log · runs.watch(fn) · await runs.ask_worker(run_id, key, question) · runs.answer/stop/resetLaufende Tasks, arbeitende Agents und ihre Logs lesen – live; einen Agent nebenbei fragen; antworten, stoppen, neu starten – Handbuch.
await ai.ask(prompt, system=, model="haiku")Eine Antwort des KI-Modells (dein Claude-Abo oder das lokale Kern-Modell).
ai.session(system=, model=) · await ai.agent(prompt, cwd=, tools=, model=)Ein Gespräch über mehrere Runden; ein eigener Claude-Code-Agent mit Werkzeugen (Standard: nur lesend).
await http.get/post/put/patch/delete(url, params=, headers=, json=, data=)HTTP-Client (.status, .json(), .text, .raise_for_status()).
log(text) · log.error · log.exception · notify(text, level)Protokoll (Einstellungen → Plugins → Protokoll) und Hinweise in der App.
tr(text) · id · version · host · language · theron_version · dirÜbersetzung und Umgebung; host ist desktop, slave oder cli.

Callbacks dürfen async oder normale Funktionen sein – normale laufen in einem eigenen Thread und dürfen blockieren. Zeitpläne, on_start und Ereignisse laufen in der App und auf einem Slave, nicht bei einem einzelnen theron execute.

Ereignisse: app.started, app.stopping, settings.changed, run.event, run.status, run.finished, node.transition (Wechsel von Node zu Node), agent.started, agent.finished, agent.status (Zwischenstand eines arbeitenden Agents, alle paar Minuten), run.reset (Prozess ab einem Node neu gestartet), hitl.asked, hitl.answered, tasklist.created sowie …saved und …deleted für project, process, agent, tasklist, epic und requirement. Jedes Ereignis trägt source – Änderungen, die das Plugin selbst gemacht hat, bekommt es nicht (außer mit include_own=True).

Prozess-Nodes

async def push(node):
    board = node.config["board"]              # Felder aus dem Editor
    for h in node.handovers:                  # eingehende Übergaben: {"from", "text", "verdict"}
        await create_card(board, h["text"])
    if not board:
        return node.failure("No board chosen")    # ✖-Ausgang
    return node.success(f"{len(node.handovers)} cards created")

ctx.nodes.register(
    "push", label="Push to Trello", run=push, icon="▦",
    description="Creates a Trello card per hand-over",
    ai_hint="Use after results that should become Trello cards",
    fields=[{"key": "board", "type": "text", "label": "Board"},
            {"key": "list", "type": "choice", "options": ["To do", "Doing"]}],
    outputs="success_failure")                # oder "success" (nur ✔)

Der Node erscheint im Editor unter WERKZEUGE. node bringt außerdem input (Übergaben samt aktuellem Requirement als Text), task, instructions, project, run_id und tasklist_id (eine Task-Liste, die mit der Übergabe kam) mit, dazu progress(text) für die Statuszeile. node.success(text, tasklist_id=…) gibt eine Task-Liste weiter, z. B. an eine Schleife. Was hereinkommt, geht bei Erfolg mit weiter. Der KI-Designer kennt die Nodes aller installierten Plugins und setzt sie ein, wenn sie passen.

Nodes wie ein Switch: benannte Aus- und Eingänge

def routes(config):                      # Ausgänge aus den Einstellungen des Nodes
    names = [l.strip() for l in config["routes"].splitlines() if l.strip()]
    return [{"id": f"r{k}", "label": n} for k, n in enumerate(names)] + [{"id": "other", "label": "Sonstiges"}]

async def route(node):
    hit = next((o for o in node.outputs if o["label"].lower() in node.input.lower()), None)
    return node.route(hit["id"] if hit else "other")      # oder mehrere: node.route(["r0", "r1"])

ctx.nodes.register("route", label="Router", run=route, outputs=routes,
                   fields=[{"key": "routes", "type": "textarea", "default": "Bug\nFeature"}])

ctx.nodes.register("merge", label="Merge", run=merge, outputs="success",
                   inputs=[{"id": "code", "label": "Code"}, {"id": "docs", "label": "Doku"}], wait="all")

outputs nimmt neben "success_failure" und "success" eine Liste benannter Ausgänge oder eine Funktion der Node-Einstellungen. Der Editor fragt sie bei jeder Änderung neu ab und zeichnet die Ports neu, Kanten bestehender Ausgänge bleiben. node.route(id) wählt den Ausgang, mehrere Ausgänge laufen wie eine UND-Verzweigung weiter. Benannte Eingänge (inputs=…) kommen je Eingang in node.inputs an. Mit wait="all" startet der Node erst, wenn an jedem verbundenen Eingang eine Übergabe liegt, mit "any" sofort. Der KI-Designer kennt die Ports und verbindet sie (routes, input_map).

Seiten & Einstellungen

def board_page(ctx):
    from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
    page = QWidget()
    QVBoxLayout(page).addWidget(QLabel("My board"))
    return page

ctx.ui.add_page("Trello", board_page, icon="▦")     # eigener Navigationspunkt

Seiten sind PySide6-Widgets und entstehen erst, wenn das Fenster da ist. Widgets nur im UI-Thread anfassen – aus Hintergrundaufgaben mit ctx.ui.call(fn). Für Einstellungen reicht meist [[settings]] in der plugin.toml; mehr Oberfläche gibt ctx.ui.settings_page(factory) (ein Widget mit optionaler save()-Methode).

Hooks: in Abläufe eingreifen

Ereignisse sagen, was passiert ist – Hooks verändern es. An festen Stellen wartet Theron auf die Plugins: Jeder Handler bekommt den Kontext (hook.context: Lauf, Projekt, Prozess, Schritt, Agent, Übergaben …) und die Daten (hook.data), die er ändern darf. Die Plugins sind nacheinander an der Reihe – in der Reihenfolge, die du unter Einstellungen → Plugins mit ▲ ▼ festlegst (oder theron plugin order); jeder sieht die Änderungen der vorigen. Ein Handler, der abbricht oder zu lange braucht, ändert nichts.

HookWas sich ändern lässt
agent.before_start · agent.after_endPrompt und Briefing eines Agents – oder er wird übersprungen (result); sein Ergebnis und Urteil.
hitl.questionFrage, Optionen, Empfehlung – oder gleich die Antwort (answer), z. B. von einer KI.
message.user · orchestrator.promptWas Theron von einer Nachricht bekommt; Therons eigenes Briefing.
process.design.before/after · agent.design.before/afterAnfrage, Prompt und Anweisungen der KI-Erstellung (z. B. ein Basis-Prompt); der fertige Entwurf.
tasklist.before_createDer Entwurf einer temporären oder befüllten Task-Liste.
assistant.process.before_create/createdAuftrag und Temp-Prozess des THERON-Assistenten.
@ctx.hooks.on("hitl.question", timeout=180)          # Rückfragen von Claude beantworten lassen
async def answer(hook):
    reply = await ctx.ai.ask(f"{hook.data['question']}\nOptionen: {hook.data['options']}\nNur die Option oder UNSURE.")
    if reply.strip() in hook.data["options"]:
        hook.data["answer"] = reply.strip()

@ctx.events.on("node.transition")                    # bei jedem Node-Wechsel eine WhatsApp
async def tell(e):
    await whatsapp(f"{e['from']['name']} → {e['to']['name']}")

Plugins bieten auch eigene Hooks und Ereignisse an (await ctx.hooks.call("mein-plugin.vor_dem_senden", …), ctx.events.emit("mein-plugin.gesendet", …)). Hooks laufen überall, wo Theron arbeitet – auch in Konsolenläufen. Jeder Hook mit Kontext und Daten: docs/hooks.md.

Anbindungen & Sync

@ctx.events.on("requirement.saved")
async def to_trello(event):                       # Theron → Trello
    req = event["data"]
    card = ctx.storage.get(f"req:{event['id']}")
    ...

async def from_trello():                          # Trello → Theron
    resp = await ctx.http.get("https://api.trello.com/1/boards/ID/cards",
                              params={"key": ctx.settings["api_key"], "token": ctx.settings["token"]})
    for card in resp.raise_for_status().json():
        ...                                       # ctx.tasklists.add_requirement / update_requirement

ctx.schedule(ctx.settings.get("interval", 5) * 60, from_trello, first=10)

Die Zuordnung (Karte ↔ Requirement) hältst du in ctx.storage. Was das Plugin über ctx.tasklists ändert, löst bei ihm selbst keine Ereignisse aus – so entstehen keine Endlosschleifen. Die Vorlage enthält eine fertige Grundlage für eine Zwei-Wege-Synchronisierung.

Werkzeuge für Agents

async def create_card(args):
    ...
    return f"Card created: {url}"

ctx.tools.register("create_card", description="Create a Trello card",
                   params={"title": "string", "description": "string"}, run=create_card)

Jeder Claude-Agent kann das Werkzeug als mcp__theron_plugins__trello_sync__create_card aufrufen. OpenCode-Agents bekommen keine Plugin-Werkzeuge.

Packen & signieren

theron plugin keygen ~/.theron-signing.key        # einmal – geheim halten
theron plugin pack my-plugin --sign ~/.theron-signing.key
theron plugin trust "ÖFFENTLICHER_SCHLÜSSEL" --name "Ich"   # auf jedem Rechner, der dir vertraut

Die Vorlage bringt mit python scripts/pack.py ein eigenes Pack-Skript mit, das ohne installiertes Theron funktioniert. Die Signatur deckt jede Datei des Plugins ab; wer danach etwas ändert, macht sie ungültig.

Fehlersuche

  • Protokoll: Einstellungen → Plugins → Protokoll, oder die Datei plugin-data/<id>/plugin.log im Datenordner. Auf einem Slave stehen die Zeilen auch in der Konsole.
  • Fehler beim Laden stehen in der Plugin-Liste; Theron läuft ohne das Plugin weiter.
  • „passt nicht zu dieser Theron-Version“: theron_api oder min_theron passt nicht – Theron aktualisieren.
  • Tests ohne Theron: Die Vorlage enthält einen nachgebauten ctx, mit dem du dein Plugin mit pytest testest.

Contents

Overview Install & manage Security & signatures Build a plugin plugin.toml The plugin API (ctx) Process nodes Pages & settings Hooks: changing what Theron does Integrations & sync Tools for agents Pack & sign Troubleshooting

Plugins · Overview & reference

On this page: what plugins can do, how to install, update and manage them (also the official plugins), what signatures and permissions mean – and, to look things up, the reference: plugin.toml, every ctx call, nodes, hooks, packing & signing.
Want to build a plugin yourself? Start with Plugins · Build – the handbook with step-by-step examples – and look things up here.

Overview

Plugins add what you are missing to Theron – without waiting for a new release. A plugin is a Python package with a plugin.toml, packed as a .theronplugin file.

  • New navigation entries with a UI of their own (e.g. a Trello board inside Theron).
  • Additional settings – generated from the plugin.toml, with a custom UI if you like.
  • New process nodes – in the editor like the built-in ones, with fields, ✔/✖ or named outputs (e.g. a switch), several inputs and support by the AI designer.
  • Integrations – e.g. sync task lists with Trello, Jira or GitHub Issues: access to task lists, events, schedules and HTTP.
  • Tools for agents – extra tools every Claude agent can use while it works.
  • Hooks – step into Theron's flow and change the outcome: agent prompts and results, questions (or their answer), AI designs, task lists, the THERON assistant's team.
  • Processes & agents – read, create, change and delete them – and talk to Claude yourself: single answers, conversations, Claude Code agents of your own.

A template with examples for all of it – including a Trello sync – is public on GitHub: github.com/mibexx/theron-plugin-sekeleton.

Ready-made plugins to download – with search and descriptions – are in the plugin catalog. The plugin manual explains step by step how to add pages, nodes, windows, buttons and form fields, work with task lists, react to events and transactions and read the running agents.

Install & manage

  • Official plugins (0.18+): Settings → Plugins → “Official plugins …” lists every plugin by mibexx – straight from the plugin server plugins.theron.mibexx.de. One click installs or updates; any older version can be picked, too. Theron takes the newest version that fits your Theron, checks the checksum and the official signature, and marks updates in the plugin list (⬆).
  • In the app: Settings → Plugins → “Upload plugin …” and pick the .theronplugin file. Theron shows name, version, signature and what the plugin uses before you install it. There you switch plugins on and off, open their settings and log, or remove them.
  • In the console (on a slave, too): theron plugin install FILE – or an official plugin by its id: theron plugin install elevenlabs, theron plugin update.
  • In the headless worker (Docker): put .theronplugin files into /data/plugin-packages – they are installed or updated at every start. Or docker exec theron-worker theron plugin install /data/x.theronplugin --yes.
  • Restart: new, changed and removed plugins take effect after Theron restarts.
  • Where it runs: a plugin node runs where the process runs. If your app controls a slave, install the plugin on the slave. Without it the node stays in the process and Theron asks when the run gets there.
CommandEffect
theron plugin listInstalled plugins with state and signature.
theron plugin info FILE|IDManifest, permissions, settings, signature.
theron plugin install FILE [--yes]Install or update (.theronplugin, .zip or a folder).
theron plugin officialThe official plugins: what is installed, what has an update.
theron plugin install ID[@VERSION]An official plugin from the plugin server – without a version the newest that fits.
theron plugin update [ID …]Update official plugins (without ids: all that have an update).
theron plugin enable|disable IDSwitch on or off.
theron plugin remove ID [--purge]Remove; --purge also deletes its settings and data.
theron plugin new FOLDERA minimal plugin to start from.
theron plugin pack FOLDER [--sign KEY] [--out TARGET]Build ID-VERSION.theronplugin, signed if you like.
theron plugin keygen FILECreate a signing key.
theron plugin trust PUBLIC_KEY [--name NAME]Plugins signed with this key count as verified.

Security & signatures

Plugins run inside Theron with the same rights as Theron itself – Python can't be sandboxed. The permissions in plugin.toml (network, tasklists …) tell you what a plugin uses; they are information, not a lock. Only install plugins whose origin you trust.

  • ✔ verified: signed with a key Theron trusts – mibexx's Theron key or one you added with theron plugin trust.
  • signed, unknown key: unchanged since it was signed, but by someone Theron doesn't know. The fingerprint is shown.
  • not signed: Theron explicitly asks whether you trust the plugin.
  • ✖ invalid: changed after signing – never installed.

If a plugin fails while loading, Theron switches it off with its error; all others keep running. A plugin that took Theron down while loading is switched off at the next start. With THERON_NO_PLUGINS=1 or --no-plugins Theron starts without any plugins.

Build a plugin

Fastest with the template: git clone https://github.com/mibexx/theron-plugin-sekeleton my-plugin – or tiny with theron plugin new my-plugin. Layout:

my-plugin/
├── plugin.toml          manifest: id, version, permissions, settings
├── my_plugin/           your Python package
│   └── __init__.py      def register(ctx): …
├── lib/                 optional: pure-Python dependencies
└── i18n/de.json         optional: translations of your texts

At start Theron calls register(ctx). Through ctx the plugin registers what it brings – it is the only interface to Theron and stays stable within an API version.

def register(ctx):
    async def hello(node):
        node.progress("greeting …")
        return node.success(f"Hello – the input had {len(node.input)} characters.")

    ctx.nodes.register("hello", label=ctx.tr("Say hello"), run=hello, outputs="success")
  • Python: plugins ship as source and run on the Python inside Theron (3.12 or newer). The whole standard library is available.
  • Dependencies: there is no pip inside Theron. Put pure-Python libraries into lib/ (pip install --target lib PACKAGE); libraries with native code don't work. For HTTP there is ctx.http.
  • Imports inside the plugin are relative (from .trello import Client).
  • Texts are English; ctx.tr("…") translates them with i18n/<language>.json.

plugin.toml

id = "trello-sync"             # lower case, digits, "-" – unique
name = "Trello Sync"
version = "1.0.0"
description = "Syncs task lists with a Trello board."
author = "Your name"
license = "MIT"
theron_api = 1                 # the plugin API it was written for
min_theron = "0.14.0"          # optional
entry = "trello_sync"          # the Python package with register(ctx)
permissions = ["network", "tasklists"]

[[settings]]
key = "api_key"
type = "secret"                # text, secret, textarea, number, bool, choice, tasklist, project
label = "API key"
required = true

Permissions: network, tasklists, processes, agents, runs, agent-tools, hooks, ui, files, ai. Every setting shows up in the plugin's settings dialog; tasklist and project become drop-downs.

The plugin API (ctx)

ctx.…What for
settings.get(key) · settings[key] · settings.set(key, value) · settings.on_change(fn) · settings.add_button(label, run) · settings.on_edit(run, keys)The plugin's settings; buttons in its settings dialog, e.g. “Test” (run(values) → text or (ok, text)); on_edit reacts while fields of the dialog change – check, ask, fill other fields ({"values": {…}, "ok", "text"}, 0.20+). Choice options may be {value, label}.
storage.get/set/delete/keys/itemsSmall persistent store (key → JSON), e.g. Trello card ↔ requirement.
data_dirA folder for files; kept across updates.
nodes.register(kind, label=, run=, fields=, outputs=, inputs=, wait=, description=, ai_hint=, icon=)A process node – ✔/✖ or named outputs/inputs (fixed or from its settings); choice fields with options from a function, fields with show_if.
{"id", "label", "color"} on outputs/inputsThe colour of the port and its edges, chosen freely by the plugin: success (green), error (red, dashed), neutral, info or #rrggbb – may follow the settings (0.18+).
"buttons": [{"label", "run"}] on a fieldButtons next to a node field, e.g. ▶ Sample: run(config) runs with the current settings, the label may be a function (▶ / ■) (0.18+).
tools.register(name, description=, params=, run=)A tool for Claude agents.
ui.add_page(title, factory, icon=) · ui.settings_page(factory) · ui.call(fn) · ui.open_url(url)UI (desktop only – ui.available).
ui.add_panel(page, title, factory, area=) · ui.open_window(title, factory, id=)Windows next to / below any page (following what the page shows) and windows of their own – manual.
ui.add_action(page, label, run, place=) · ui.add_form_button(form, label, run) · fields.add(form, key, …)Buttons in tool bars, next to “Send”, next to “+ New …”, on the task-list page, in the sidebar; fields and buttons in the agent, process and project forms – manual.
ui.ask_text · ui.ask_choice · ui.message · ui.chat_input · ui.set_chat_input · ui.ask_files · ui.ask_folder · await ui.run(fn)Small dialogs in Theron's look (ask_text with a list of suggestions, too); file and folder pickers; ui.run runs something on the UI thread from nodes and buttons and returns its result (0.20+); read and fill the chat input.
chat.add_format(pattern, render) · chat.add_renderer(kind, render) · chat.link(name, text, …) · chat.on_link(name, fn) · chat.refresh()Extend the chat: text in any message drawn differently, blocks of your own (players, documents, diagrams …) with clickable buttons – the plugin renders, Theron offers the hooks – manual.
await node.ask(question, options) · node.say(text, kind=, data=)A node asks the user in the chat and waits for the answer; a node posts into the chat (text or a block of its own).
events.on(name, fn)React to events (see below).
schedule(seconds, fn, first=) · on_start(fn) · on_stop(fn)Recurring jobs, start and stop.
spawn(fn, *args)Run something in the background, e.g. after a click (returns a future).
tasklists.projects/list/get/find/create/update/delete, add_epic/update_epic/delete_epic, add_requirement/update_requirement/delete_requirementTask lists, epics and requirements.
processes.list/get/find/create/create_from_draft/to_draft/update/delete · agents.list/get/find/create/update/deleteRead, create, change and delete processes and agents.
hooks.on(name, fn, timeout=) · await hooks.call(name, data) · events.emit(name, payload)Step into the flow; offer hooks and events of your own.
runs.start(project_id, text, title=) · runs.get/list · runs.messageStart and read tasks.
runs.active/state/workers/events/log · runs.watch(fn) · await runs.ask_worker(run_id, key, question) · runs.answer/stop/resetRead running tasks, working agents and their logs – live; ask an agent on the side; answer, stop, restart – manual.
await ai.ask(prompt, system=, model="haiku")One answer of the AI model (your Claude subscription or the local core model).
ai.session(system=, model=) · await ai.agent(prompt, cwd=, tools=, model=)A conversation over several turns; a Claude Code agent of your own with tools (read-only by default).
await http.get/post/put/patch/delete(url, params=, headers=, json=, data=)HTTP client (.status, .json(), .text, .raise_for_status()).
log(text) · log.error · log.exception · notify(text, level)Log (Settings → Plugins → Log) and notes in the app.
tr(text) · id · version · host · language · theron_version · dirTranslation and environment; host is desktop, slave or cli.

Callbacks may be async or plain functions – plain ones run in a worker thread and may block. Schedules, on_start and events run in the app and on a slave, not during a single theron execute.

Events: app.started, app.stopping, settings.changed, run.event, run.status, run.finished, node.transition (moving from node to node), agent.started, agent.finished, agent.status (a working agent's progress, every few minutes), run.reset (process restarted from a node), hitl.asked, hitl.answered, tasklist.created plus …saved and …deleted for project, process, agent, tasklist, epic and requirement. Every event carries source – changes the plugin made itself don't reach it (unless include_own=True).

Process nodes

async def push(node):
    board = node.config["board"]              # fields from the editor
    for h in node.handovers:                  # incoming hand-overs: {"from", "text", "verdict"}
        await create_card(board, h["text"])
    if not board:
        return node.failure("No board chosen")    # ✖ output
    return node.success(f"{len(node.handovers)} cards created")

ctx.nodes.register(
    "push", label="Push to Trello", run=push, icon="▦",
    description="Creates a Trello card per hand-over",
    ai_hint="Use after results that should become Trello cards",
    fields=[{"key": "board", "type": "text", "label": "Board"},
            {"key": "list", "type": "choice", "options": ["To do", "Doing"]}],
    outputs="success_failure")                # or "success" (✔ only)

The node shows up in the editor under TOOLS. node also brings input (hand-overs plus the current requirement as text), task, instructions, project, run_id and tasklist_id (a task list that came with the hand-over), plus progress(text) for the status line. node.success(text, tasklist_id=…) hands a task list on, e.g. to a loop. On success, what came in goes on, too. The AI designer knows the nodes of all installed plugins and uses them where they fit.

Switch-like nodes: named outputs and inputs

def routes(config):                      # outputs from the node's settings
    names = [l.strip() for l in config["routes"].splitlines() if l.strip()]
    return [{"id": f"r{k}", "label": n} for k, n in enumerate(names)] + [{"id": "other", "label": "Other"}]

async def route(node):
    hit = next((o for o in node.outputs if o["label"].lower() in node.input.lower()), None)
    return node.route(hit["id"] if hit else "other")      # or several: node.route(["r0", "r1"])

ctx.nodes.register("route", label="Router", run=route, outputs=routes,
                   fields=[{"key": "routes", "type": "textarea", "default": "Bug\nFeature"}])

ctx.nodes.register("merge", label="Merge", run=merge, outputs="success",
                   inputs=[{"id": "code", "label": "Code"}, {"id": "docs", "label": "Docs"}], wait="all")

Besides "success_failure" and "success", outputs takes a list of named outputs or a function of the node's settings – the editor asks it again on every change and redraws the ports; edges of outputs that still exist stay. node.route(id) picks the output; several outputs go on like an AND split. Named inputs (inputs=…) arrive per input in node.inputs; with wait="all" the node starts once every connected input has a hand-over, with "any" right away. The AI designer knows the ports and connects them (routes, input_map).

Pages & settings

def board_page(ctx):
    from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
    page = QWidget()
    QVBoxLayout(page).addWidget(QLabel("My board"))
    return page

ctx.ui.add_page("Trello", board_page, icon="▦")     # a navigation entry of its own

Pages are PySide6 widgets, built once the window exists. Only touch widgets on the UI thread – from background jobs use ctx.ui.call(fn). For settings, [[settings]] in plugin.toml is usually enough; more UI comes with ctx.ui.settings_page(factory) (a widget with an optional save() method).

Hooks: changing what Theron does

Events tell you what happened – hooks change it. At fixed points Theron waits for the plugins: every handler gets the context (hook.context: run, project, process, step, agent, hand-overs …) and the data it may change (hook.data). The plugins take turns one after another – in the order you set under Settings → Plugins with ▲ ▼ (or theron plugin order); each sees the changes of the ones before. A handler that fails or takes too long changes nothing.

HookWhat can be changed
agent.before_start · agent.after_endAn agent's prompt and briefing – or it is skipped (result); its result and verdict.
hitl.questionQuestion, options, recommendation – or the answer itself (answer), e.g. from an AI.
message.user · orchestrator.promptWhat Theron gets from a message; Theron's own briefing.
process.design.before/after · agent.design.before/afterRequest, prompt and instructions of the AI design (e.g. a base prompt); the finished draft.
tasklist.before_createThe draft of a temporary or filled task list.
assistant.process.before_create/createdThe THERON assistant's job and temporary process.
@ctx.hooks.on("hitl.question", timeout=180)          # let Claude answer questions
async def answer(hook):
    reply = await ctx.ai.ask(f"{hook.data['question']}\nOptions: {hook.data['options']}\nThe option only, or UNSURE.")
    if reply.strip() in hook.data["options"]:
        hook.data["answer"] = reply.strip()

@ctx.events.on("node.transition")                    # a WhatsApp on every node change
async def tell(e):
    await whatsapp(f"{e['from']['name']} → {e['to']['name']}")

Plugins can offer hooks and events of their own (await ctx.hooks.call("my-plugin.before_send", …), ctx.events.emit("my-plugin.sent", …)). Hooks run wherever Theron works – console runs included. Every hook with its context and data: docs/hooks.md.

Integrations & sync

@ctx.events.on("requirement.saved")
async def to_trello(event):                       # Theron → Trello
    req = event["data"]
    card = ctx.storage.get(f"req:{event['id']}")
    ...

async def from_trello():                          # Trello → Theron
    resp = await ctx.http.get("https://api.trello.com/1/boards/ID/cards",
                              params={"key": ctx.settings["api_key"], "token": ctx.settings["token"]})
    for card in resp.raise_for_status().json():
        ...                                       # ctx.tasklists.add_requirement / update_requirement

ctx.schedule(ctx.settings.get("interval", 5) * 60, from_trello, first=10)

Keep the mapping (card ↔ requirement) in ctx.storage. Changes the plugin makes through ctx.tasklists don't fire events back at it – no endless loops. The template contains a ready base for a two-way sync.

Tools for agents

async def create_card(args):
    ...
    return f"Card created: {url}"

ctx.tools.register("create_card", description="Create a Trello card",
                   params={"title": "string", "description": "string"}, run=create_card)

Every Claude agent can call it as mcp__theron_plugins__trello_sync__create_card. OpenCode agents don't get plugin tools.

Pack & sign

theron plugin keygen ~/.theron-signing.key        # once – keep it secret
theron plugin pack my-plugin --sign ~/.theron-signing.key
theron plugin trust "PUBLIC_KEY" --name "Me"      # on every machine that should trust you

The template comes with python scripts/pack.py, a pack script that works without Theron installed. The signature covers every file of the plugin; changing anything afterwards makes it invalid.

Troubleshooting

  • Log: Settings → Plugins → Log, or the file plugin-data/<id>/plugin.log in the data folder. On a slave the lines are printed to the console, too.
  • Errors while loading show up in the plugin list; Theron runs on without the plugin.
  • “doesn't fit this Theron version”: theron_api or min_theron doesn't match – update Theron.
  • Tests without Theron: the template contains a stand-in ctx to test your plugin with pytest.
© 2026 Mike Bertram · mibexx Imprint API documentation