Erstelle ein Plugin in 5 Minuten
Alles läuft mit JavaScript — das SDK ist in Yoki integriert, kein npm install nötig. Diese Anleitung führt dich durch die Erstellung deines ersten Plugins.
Schritt 1: Erstelle einen Ordner
Jedes Plugin hat seinen eigenen Ordner in ~/yoki/plugins/. Erstelle einen:
mkdir ~/yoki/plugins/my-pluginOder nutze die CLI für automatische Generierung:
npx create-yoki-pluginGeneriert plugin.json + main.js sofort einsatzbereitSchritt 2: Füge plugin.json hinzu
Diese Datei beschreibt dein Plugin — Name, Schlüsselwort und welche Skripte ausgeführt werden.
{
"name": "My Plugin",
"keyword": "my",
"icon": "star",
"version": "1.0.0",
"protocol": "v2",
"commands": [
{
"name": "main",
"title": "My Plugin",
"mode": "detail",
"exec": "main.js",
"takes_query": true
}
]
}Schritt 3: Schreibe dein Skript
Dein Skript liest Eingaben von Yoki, verarbeitet sie und sendet eine Antwort zurück.
const { readInput, writeResponse, detail } = require("@yoki/plugin-sdk")
async function main() {
const input = await readInput()
const query = input.query || ""
if (!query) {
writeResponse(detail('<div style="padding:16px;color:#888">Type something after <b>my</b></div>'))
return
}
writeResponse(detail(
`<div style="padding:16px">
<div style="font-size:24px;font-weight:bold;color:#4FC3F7">${query.toUpperCase()}</div>
<div style="margin-top:8px;color:#888">Your input, uppercased</div>
</div>`,
[{ label: "Original", value: query }, { label: "Result", value: query.toUpperCase() }],
[{ title: "Copy result", type: "copy", value: query.toUpperCase() }]
))
}
main()Schritt 4: Teste es
Teste ohne Yoki — leite JSON in dein Skript:
echo '{"query":"hello world","command":"main","context":{}}' | node main.jsDu solltest sehen:
{"type":"detail","markdown":"<div style=\"padding:16px\"><div style=\"font-size:24px;font-weight:bold;color:#4FC3F7\">HELLO WORLD</div><div style=\"margin-top:8px;color:#888\">Your input, uppercased</div></div>","metadata":[{"label":"Original","value":"hello world"},{"label":"Result","value":"HELLO WORLD"}],"actions":[{"title":"Copy result","type":"copy","value":"HELLO WORLD"}]}Öffne Yoki, tippe my hello world — du siehst eine Karte mit „HELLO WORLD". Enter kopiert.
Schritt 5: Was kommt als nächstes
Lies weiter für die vollständige SDK-Referenz.
SDK-Referenz
Jede Funktion im Yoki SDK mit echten Beispielen. Kein Install nötig.
const { readInput, writeResponse, detail, list, background, error, stripKeyword, escHtml } = require("@yoki/plugin-sdk")readInput() — Reads what the user typed and context from Yoki.writeResponse(resp) — Sends your response back to Yoki.detail(html, metadata?, actions?) — Shows a rich card with HTML content, a metadata sidebar, and action buttons.list(items) — Shows a scrollable list of items.background(hud, notification?) — Runs an action without showing any UI.error(message, details?) — Shows an error message to the user.stripKeyword(query, ...keywords) — Removes a keyword prefix from the query string.escHtml(str) — Escapes HTML special characters (&, <, >, ").readInput()
Reads what the user typed and context from Yoki. Call this once at the start of your script. Returns an object with the user's query, which command was triggered, and context (paths, locale, credentials).
async readInput(): Promise<object>const input = await readInput()
const query = input.query // "hello world"const input = await readInput()
const dataDir = input.context.data_dir
// e.g. "C:/Users/you/yoki/plugins/my-plugin/data"
// Use this to store config, cache, tokens — it persists across runsconst input = await readInput()
const apiKey = input.context.credentials?.api_key || ""
if (!apiKey) {
writeResponse(error("API key not configured"))
return
}// input = {
// query: "hello world", — what the user typed after keyword
// command: "main", — which command (from plugin.json)
// action: "search", — "search" | "execute" | "refresh"
// context: {
// yoki_version: "1.0.8.2",
// os: "windows",
// locale: "en", — user's language
// plugin_dir: "C:/.../my-plugin",
// data_dir: "C:/.../my-plugin/data",
// credentials: { ... } — from credentials service
// },
// preferences: {}
// }writeResponse(resp)
Sends your response back to Yoki. Call this once at the end of your script. Pass the result from any builder function — detail(), list(), error(), etc.
writeResponse(resp: object): voidwriteResponse(detail("<div>Hello!</div>"))writeResponse(error("Something went wrong", "Please try again later"))detail(html, metadata?, actions?)
Shows a rich card with HTML content, a metadata sidebar, and action buttons. When the user presses Enter, the first action with type "copy" is triggered.
detail(
html: string,
metadata?: [{ label: string, value: string }, ...],
actions?: [{ title: string, type: string, value?: string }, ...]
)writeResponse(detail(
'<div style="padding:16px;font-size:20px">Hello, World!</div>'
))writeResponse(detail(
'<div style="padding:16px"><h2>2 + 2 = 4</h2></div>',
[
{ label: "Hex", value: "0x4" },
{ label: "Binary", value: "0b100" },
{ label: "Prime", value: "No" }
]
))writeResponse(detail(
'<div style="padding:16px;font-size:24px;color:#4FC3F7">42</div>',
[{ label: "Expression", value: "6 * 7" }],
[{ title: "Copy result", type: "copy", value: "42" }]
))writeResponse(detail(
'<div style="padding:16px"><h2>Now Playing</h2><p>Mind Mirage — Windows 96</p></div>',
[{ label: "Duration", value: "5:00" }],
[
{ title: "Pause", type: "yoki_run", value: "sp pause", variant: "primary" },
{ title: "Next", type: "yoki_run", value: "sp next" },
{ title: "Copy track", type: "copy", value: "Mind Mirage — Windows 96" },
{ title: "Open in Spotify", type: "open_url", url: "https://open.spotify.com/track/..." }
]
))list(items)
Shows a scrollable list of items. Each item has a title, subtitle, icon, and optional actions. Use for search results, bookmarks, file lists.
list(items: [{
id: string, title: string, subtitle?: string,
icon?: string, actions?: Action[]
}, ...])writeResponse(list([
{ id: "1", title: "First result", subtitle: "Description here" },
{ id: "2", title: "Second result", subtitle: "Another description" },
]))writeResponse(list([
{
id: "gh",
title: "GitHub",
subtitle: "https://github.com · Bookmarks Bar",
icon: "🌐",
actions: [
{ title: "Open", shortcut: "enter", type: "open_url", url: "https://github.com" },
{ title: "Copy URL", shortcut: "cmd+c", type: "copy", value: "https://github.com" }
]
}
]))const input = await readInput()
const query = input.query.toLowerCase()
const allItems = [
{ id: "1", title: "GitHub", subtitle: "Code hosting" },
{ id: "2", title: "Google", subtitle: "Search engine" },
{ id: "3", title: "Gmail", subtitle: "Email" },
]
const filtered = query
? allItems.filter(i => i.title.toLowerCase().includes(query))
: allItems
writeResponse(list(filtered))background(hud, notification?)
Runs an action without showing any UI. Shows a brief HUD toast. Optionally sends a system notification. The user must press Enter to trigger it (never fires on keystroke). Use for play/pause, save, toggle.
background(hud: string, notification?: { title: string, body: string })writeResponse(background("Done!"))writeResponse(background(
"Playing: Mind Mirage",
{ title: "Spotify", body: "Now playing: Mind Mirage — Windows 96" }
))error(message, details?)
Shows an error message to the user. Use when something goes wrong — missing input, API failure, runtime not installed. Plugin crashes are also caught automatically.
error(message: string, details?: string)writeResponse(error("Not found"))writeResponse(error("API request failed", "Check your internet connection and try again"))const input = await readInput()
if (!input.query) {
writeResponse(error("No query", "Type something after the keyword"))
return
}stripKeyword(query, ...keywords)
Removes a keyword prefix from the query string. Useful when your plugin has subcommands (e.g. "search", "play", "list").
stripKeyword(query: string, ...keywords: string[]): stringstripKeyword("search hello world", "search", "s")
// → "hello world"
stripKeyword("s hello world", "search", "s")
// → "hello world" (matches short alias too)
stripKeyword("search", "search", "s")
// → "" (exact match returns empty string)escHtml(str)
Escapes HTML special characters (&, <, >, "). Use this when inserting user input into your HTML to prevent broken rendering or XSS.
escHtml(s: string): stringconst input = await readInput()
const safe = escHtml(input.query)
writeResponse(detail(`<div>${safe}</div>`))
// input: "<b>bold</b>"
// safe: "<b>bold</b>"
// renders as text, not HTMLProtokoll v2
Wenn der Nutzer dein Schlüsselwort tippt, startet Yoki dein Skript, schreibt V2Input JSON nach stdin und liest V2Response von stdout. Verstecktes Fenster, 5-Sekunden-Timeout (konfigurierbar).
{
"version": "2.0",
"command": "greet",
"action": "search",
"query": "Ada",
"context": {
"yoki_version": "1.0.8.2",
"os": "windows",
"locale": "en",
"plugin_dir": "C:/Users/you/yoki/plugins/hello",
"data_dir": "C:/Users/you/yoki/plugins/hello/data",
"credentials": {}
},
"preferences": {}
}{
"type": "detail",
"markdown": "<div style=\"padding:16px\"><h2>Hello, Ada!</h2></div>",
"metadata": [
{ "label": "Name", "value": "Ada" }
],
"actions": [
{ "title": "Copy", "type": "copy", "value": "Hello, Ada!" }
]
}V2Input-Felder
Antwortmodi
Das type-Feld bestimmt wie Yoki deine Antwort rendert.
Scrollbare Elemente mit Icon, Titel, Untertitel und Aktionen.
HTML-Karte mit Metadaten-Seitenleiste und Aktions-Buttons. Enter kopiert die erste Copy-Aktion.
Visuelles Raster mit konfigurierbaren Spalten.
Kein UI — Seiteneffekt mit HUD-Toast. Enter-gesteuert.
Mehrteiliges Eingabeformular. Kommt bald.
Fehler mit optionalen Details. Plugin-Abstürze werden automatisch abgefangen.
detail mode
writeResponse(detail(
'<div style="padding:16px"><h2>Now Playing</h2><p>Mind Mirage — Windows 96</p></div>',
[{ label: "Duration", value: "5:00" }, { label: "Album", value: "Enchanted Instrumentals" }],
[{ title: "Pause", type: "yoki_run", value: "sp pause", variant: "primary" },
{ title: "Copy", type: "copy", value: "Mind Mirage" }]
))list mode
writeResponse(list([
{ id: "1", title: "Mind Mirage", subtitle: "Windows 96 · 5:00", icon: "music",
actions: [{ title: "Play", type: "yoki_run", value: "sp play spotify:track:xyz" },
{ title: "Copy", type: "copy", value: "Mind Mirage" }] }
]))background mode
writeResponse(background("Playing: Mind Mirage", { title: "Spotify", body: "Now playing" }))Berechtigungen
Plugins deklarieren was sie brauchen. Yoki zeigt einen Zustimmungsdialog.
Zugangsdaten
Plugins sollten niemals API-Schlüssel einbetten. Deklariere was du brauchst.
{
"name": "Spotify",
"keyword": "sp",
"protocol": "v2",
"credentials": {
"slug": "spotify",
"service": "spotify",
"keys": ["client_id"]
},
"commands": [...]
}# Plugin reads credentials from V2Input.context — never embeds them.
import sys, json
inp = json.loads(sys.stdin.read())
client_id = (inp.get("context") or {}).get("credentials", {}).get("client_id")
if not client_id:
# Yoki couldn't fetch creds (offline / not signed in) — surface a
# graceful setup-needed response instead of crashing.
print(json.dumps({"type": "error", "error": "Sign in to Yoki to use this plugin"}))
sys.exit(0)
# ... rest of plugin uses client_id ...Manifest-Referenz
Alle Felder die Yoki versteht. $schema für IDE-Autovervollständigung.
{
"$schema": "https://raw.githubusercontent.com/yoki-run/yoki/main/plugin.schema.json",
"name": "string (required) display name",
"keyword": "string (required) trigger word, e.g. 'sp'",
"icon": "string icon name or emoji",
"description": "string shown in Plugins Manager",
"version": "string semver, e.g. '1.0.0'",
"author": "string",
"homepage": "string url",
"license": "string spdx, e.g. 'MIT'",
"categories": "string[] e.g. ['media', 'tools']",
"yoki_min": "string min Yoki version, e.g. '1.0.8.0'",
"protocol": "'v2'",
"permissions": {
"network": "string[] hostname whitelist for HTTPS",
"filesystem": "string[] 'read:./data', 'write:./data'",
"clipboard": "boolean",
"notifications": "boolean",
"shell": "boolean",
"ai": "boolean"
},
"credentials": {
"slug": "string defaults to keyword",
"service": "string informational",
"keys": "string[] credential names"
},
"commands": [
{
"name": "string (required) internal id",
"title": "string (required) user-facing label",
"mode": "'list' | 'detail' | 'grid' | 'background' | 'form'",
"exec": "string (required) script path relative to plugin dir",
"keywords": "string[] sub-keyword aliases",
"takes_query": "boolean true = query passed verbatim",
"refresh": "number ms — auto re-run interval",
"timeout": "number ms — default 5000"
}
]
}Aktionstypen
Setze action.type für Buttons oder Listen-Aktionen:
Aktions-Icons
Drei Formen für das icon-Feld:
Plugin-Erkennung
Lokales Testen
# Teste jedes Plugin ohne Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.jsShowcase
Offizielle Plugins mit dem JavaScript SDK. Klone eines als Startpunkt.
spVolle Spotify-Steuerung — aktueller Track, Suche, Wiedergabe, Geräte, Playlists.
- OAuth 2.0 PKCE in pure stdlib
- Auto-refresh detail card
- Inline SVG icons
- Credentials service integration
mathErweiterter Rechner — Trigonometrie, Integrale, Ableitungen, ASCII-Plots.
- Safe expression parser (no eval)
- Symbolic differentiation (chain rule)
- Numerical integration (Simpson)
- ASCII function plots
bmSofortige Suche in Chrome, Edge, Brave, Vivaldi Lesezeichen.
- Reads local bookmark files (no API)
- Multi-browser detection
- Multi-word fuzzy search
- Open in browser or copy URL
Was kommt
Das Plugin-System ist produktionsreif.