Yoki Plugin SDK

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-plugin

O usa la CLI para generar todo automáticamente:

npx create-yoki-pluginGenera plugin.json + main.js listo para usar

Paso 2: Añade plugin.json

Este archivo describe tu plugin — nombre, palabra clave y qué scripts ejecutar.

~/yoki/plugins/my-plugin/plugin.json
{
  "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
    }
  ]
}
keywordlo que el usuario escribe para activar tu plugin
modecómo mostrar el resultado. detail = tarjeta, list = lista
execqué archivo de script ejecutar
takes_querytrue = el usuario puede escribir texto después de la palabra clave

Paso 3: Escribe tu script

Tu script lee la entrada de Yoki, la procesa y envía una respuesta.

~/yoki/plugins/my-plugin/main.js
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.js

Deberí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?

Cambia mode a "list" — devuelve múltiples elementos con scroll
Añade actions — botones para copiar, abrir URLs, ejecutar comandos
Añade metadata — pares clave-valor en la barra lateral
Usa background mode — para acciones sin UI
Publica en el marketplace — sube a GitHub, envía a Yoki

Sigue leyendo para la referencia completa del SDK.

Referencia SDK

Cada función del SDK con ejemplos reales. Sin instalación.

Importar
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).

Firma
async readInput(): Promise<object>
Get the user query
const input = await readInput()
const query = input.query  // "hello world"
Access the data directory (for saving files)
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 runs
Read credentials (for API plugins)
const input = await readInput()
const apiKey = input.context.credentials?.api_key || ""
if (!apiKey) {
  writeResponse(error("API key not configured"))
  return
}
Full input structure
// 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.

Firma
writeResponse(resp: object): void
Basic usage
writeResponse(detail("<div>Hello!</div>"))
Send an error
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.

Firma
detail(
  html: string,
  metadata?: [{ label: string, value: string }, ...],
  actions?:  [{ title: string, type: string, value?: string }, ...]
)
Simple card
writeResponse(detail(
  '<div style="padding:16px;font-size:20px">Hello, World!</div>'
))
Card with metadata sidebar
writeResponse(detail(
  '<div style="padding:16px"><h2>2 + 2 = 4</h2></div>',
  [
    { label: "Hex", value: "0x4" },
    { label: "Binary", value: "0b100" },
    { label: "Prime", value: "No" }
  ]
))
Card with copy button (Enter copies the result)
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" }]
))
Card with multiple action buttons
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.

Firma
list(items: [{
  id: string, title: string, subtitle?: string,
  icon?: string, actions?: Action[]
}, ...])
Simple list
writeResponse(list([
  { id: "1", title: "First result", subtitle: "Description here" },
  { id: "2", title: "Second result", subtitle: "Another description" },
]))
List with actions (Enter opens, Cmd+C copies)
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" }
    ]
  }
]))
Filtered list (search)
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.

Firma
background(hud: string, notification?: { title: string, body: string })
Simple HUD toast
writeResponse(background("Done!"))
HUD + system notification
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.

Firma
error(message: string, details?: string)
Simple error
writeResponse(error("Not found"))
Error with details
writeResponse(error("API request failed", "Check your internet connection and try again"))
Graceful handling of missing input
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").

Firma
stripKeyword(query: string, ...keywords: string[]): string
Remove subcommand
stripKeyword("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.

Firma
escHtml(s: string): string
Escape user input before rendering
const input = await readInput()
const safe = escHtml(input.query)
writeResponse(detail(`<div>${safe}</div>`))

// input: "<b>bold</b>"
// safe:  "&lt;b&gt;bold&lt;/b&gt;"
// renders as text, not HTML

Protocolo 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).

stdin → V2Input
{
  "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": {}
}
stdout → V2Response
{
  "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

versionSiempre "2.0"
commandQué comando fue activado
action"search" (escribiendo), "execute" (Enter), "refresh" (auto-refresh)
queryEntrada completa del usuario después de la palabra clave
context.yoki_versionVersión del host
context.osSiempre "windows" por ahora
context.localeIdioma del usuario
context.plugin_dirRuta absoluta a la carpeta del plugin
context.data_dirDirectorio de datos aislado (persiste entre ejecuciones)
context.credentialsClaves del servicio de credenciales (o vacío)

Modos de respuesta

El campo type decide cómo Yoki muestra tu respuesta.

list
POR DEFECTO

Elementos desplazables con icono, título, subtítulo y acciones.

detail

Tarjeta HTML con barra lateral de metadatos y botones de acción. Enter copia la primera acción copy.

grid

Cuadrícula visual con columnas configurables.

background
EFECTO SECUNDARIO

Sin UI — efecto secundario con toast HUD. Activado por Enter.

form
PRÓXIMAMENTE

Formulario de entrada. Próximamente.

error

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.

network
Lista blanca de hosts HTTPS — solo los dominios que indiques.
filesystem
Rutas de lectura/escritura relativas al directorio de datos. Aislado.
notifications
Notificaciones toast de Windows.
clipboard
Lectura/escritura del portapapeles del sistema.

Credenciales

Los plugins nunca deben incrustar claves API. Declara lo que necesitas.

declaración del manifiesto
{
  "name": "Spotify",
  "keyword": "sp",
  "protocol": "v2",
  "credentials": {
    "slug": "spotify",
    "service": "spotify",
    "keys": ["client_id"]
  },
  "commands": [...]
}
el plugin lee del contexto
# 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 ...
Envía un PR o escribe a yokirun@yoki.run.

Referencia del manifiesto

Todos los campos que Yoki entiende. $schema para autocompletado.

plugin.json — esquema completo
{
  "$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:

copy  valueCopiar valor al portapapeles.
open_url  urlAbrir URL en el navegador predeterminado.
yoki_run  valueEjecutar una consulta Yoki.
exec  exec, argsEjecutar otro script del plugin.
paste  valuePegar texto en la ventana activa.
notification  title, bodyMostrar notificación del sistema.
close  Cerrar el lanzador Yoki.
refresh  Re-ejecutar el comando actual.

Iconos de acción

Tres formas para el campo icon:

<svg ...>...</svg> SVG inline, limpiado con DOMPurify.
data:image/svg+xml;base64,... Data URL o https:// URL — renderizado como <img>.
"file" / "folder" / "music" / ... Nombre del conjunto de iconos integrado de Yoki.

Descubrimiento de plugins

Yoki escanea ~/yoki/plugins/ al iniciar y al enfocar la ventana.
Cada subdirectorio con plugin.json es un plugin.
Los plugins tienen un directorio data/ aislado.
Los fallos de carga se muestran en el Administrador de Plugins.
Las palabras clave duplicadas son rechazadas.

Pruebas locales

# Prueba cualquier plugin sin Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.js

Ejemplos

Plugins oficiales con el SDK de JavaScript. Clona cualquiera como punto de partida.

Spotify · Node.js · sp

Control total de Spotify — reproducción actual, búsqueda, dispositivos, playlists.

sp  Now Playing card
sp play [query]  Search & play
sp pause / next / prev  Playback controls
sp s [query]  Search tracks
sp d  Devices
sp pl  Playlists
Características
  • OAuth 2.0 PKCE in pure stdlib
  • Auto-refresh detail card
  • Inline SVG icons
  • Credentials service integration
Extended Math · Node.js · math

Calculadora avanzada — trigonometría, integrales, derivadas, gráficos ASCII.

math 2^8 + sqrt(144)  Calculate
math trig table 60  Trig table
math int x^2 0 10  Integrate
math der sin(x^2)  Derivative
math plot sin(x) -pi pi  ASCII plot
Características
  • Safe expression parser (no eval)
  • Symbolic differentiation (chain rule)
  • Numerical integration (Simpson)
  • ASCII function plots
Bookmarks · Node.js · bm

Búsqueda instantánea de marcadores en Chrome, Edge, Brave, Vivaldi.

bm  List all bookmarks
bm github  Search bookmarks
Características
  • 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.

LISTO
v2 protocol + 5 response modes
stdin/stdout JSON, todos los modos de respuesta, botones de acción, auto-refresh.
LISTO
Bundled JavaScript SDK
SDKs sin instalación integrados en el binario de Yoki.
LISTO
Plugin Marketplace
Explorar en la app + instalación en un clic. Revisión curada.
LISTO
Credentials service
Los plugins declaran lo que necesitan, Yoki obtiene secretos en tiempo de ejecución.
LISTO
Scaffold CLI (npx create-yoki-plugin)
Scaffold interactivo — ejecuta npx create-yoki-plugin, plugin JavaScript funcional en segundos.
LISTO
JSON Schema for plugin.json
Autocompletado IDE para todos los campos del manifiesto.
SIGUIENTE
Plugin settings UI (form mode)
Los plugins declaran campos de configuración, Yoki renderiza una página de ajustes.
SIGUIENTE
Auto-update for plugins
Comparar versión instalada con tag del marketplace, badge de actualización.
DESPUÉS
AI tool integration
Los plugins se exponen como herramientas que Yoki AI puede llamar.
Crea tu primer plugin
SDK estable, tres plugins de referencia.
Comenzar
Yoki Plugin SDK · v2 · yoki.run