Crea un plugin en 5 minutos
Todo funciona con JavaScript — el SDK viene con Yoki, sin npm install. Esta guía te lleva paso a paso para crear tu primer plugin.
Paso 1: Crea una carpeta
Cada plugin vive en su propia carpeta dentro de ~/yoki/plugins/:
mkdir ~/yoki/plugins/my-pluginO usa la CLI para generar todo automáticamente:
npx create-yoki-pluginGenera plugin.json + main.js listo para usarPaso 2: Añade plugin.json
Este archivo describe tu plugin — nombre, palabra clave y qué scripts ejecutar.
{
"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
}
]
}Paso 3: Escribe tu script
Tu script lee la entrada de Yoki, la procesa y envía una respuesta.
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()Paso 4: Pruébalo
Prueba sin Yoki — pasa JSON a tu script:
echo '{"query":"hello world","command":"main","context":{}}' | node main.jsDeberías ver:
{"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"}]}Abre Yoki, escribe my hello world — verás una tarjeta con «HELLO WORLD». Enter copia.
Paso 5: ¿Qué sigue?
Sigue leyendo para la referencia completa del SDK.
Referencia SDK
Cada función del SDK con ejemplos reales. Sin instalación.
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 HTMLProtocolo v2
Cuando el usuario escribe tu palabra clave, Yoki ejecuta tu script, escribe V2Input JSON en stdin y lee V2Response de stdout. Ventana oculta, timeout 5 segundos (configurable).
{
"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!" }
]
}Campos V2Input
Modos de respuesta
El campo type decide cómo Yoki muestra tu respuesta.
Elementos desplazables con icono, título, subtítulo y acciones.
Tarjeta HTML con barra lateral de metadatos y botones de acción. Enter copia la primera acción copy.
Cuadrícula visual con columnas configurables.
Sin UI — efecto secundario con toast HUD. Activado por Enter.
Formulario de entrada. Próximamente.
Error con detalles opcionales. Los fallos de plugins se capturan automáticamente.
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" }))Permisos
Los plugins declaran lo que necesitan. Yoki muestra un diálogo de consentimiento.
Credenciales
Los plugins nunca deben incrustar claves API. Declara lo que necesitas.
{
"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 ...Referencia del manifiesto
Todos los campos que Yoki entiende. $schema para autocompletado.
{
"$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"
}
]
}Tipos de acción
Establece action.type en botones o acciones de lista:
Iconos de acción
Tres formas para el campo icon:
Descubrimiento de plugins
Pruebas locales
# Prueba cualquier plugin sin Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.jsEjemplos
Plugins oficiales con el SDK de JavaScript. Clona cualquiera como punto de partida.
spControl total de Spotify — reproducción actual, búsqueda, dispositivos, playlists.
- OAuth 2.0 PKCE in pure stdlib
- Auto-refresh detail card
- Inline SVG icons
- Credentials service integration
mathCalculadora avanzada — trigonometría, integrales, derivadas, gráficos ASCII.
- Safe expression parser (no eval)
- Symbolic differentiation (chain rule)
- Numerical integration (Simpson)
- ASCII function plots
bmBúsqueda instantánea de marcadores en Chrome, Edge, Brave, Vivaldi.
- Reads local bookmark files (no API)
- Multi-browser detection
- Multi-word fuzzy search
- Open in browser or copy URL
Qué sigue
El sistema de plugins está listo para producción.