Yoki Plugin SDK

Crea un plugin in 5 minuti

Tutto funziona con JavaScript — l'SDK è integrato in Yoki, nessun npm install necessario. Questa guida ti porta passo passo nella creazione del tuo primo plugin.

Passo 1: Crea una cartella

Ogni plugin ha la sua cartella in ~/yoki/plugins/:

mkdir ~/yoki/plugins/my-plugin

Oppure usa la CLI per generare tutto automaticamente:

npx create-yoki-pluginGenera plugin.json + main.js pronto all'uso

Passo 2: Aggiungi plugin.json

Questo file descrive il tuo plugin — nome, parola chiave e script da eseguire.

~/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
    }
  ]
}
keywordcosa l'utente digita per attivare il plugin
modecome mostrare il risultato. detail = scheda, list = lista
execquale file script eseguire
takes_querytrue = l'utente può digitare testo dopo la parola chiave

Passo 3: Scrivi il tuo script

Lo script legge l'input da Yoki, lo elabora e restituisce una risposta.

~/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()

Passo 4: Testalo

Testa senza Yoki — passa JSON al tuo script:

echo '{"query":"hello world","command":"main","context":{}}' | node main.js

Dovresti vedere:

{"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"}]}

Apri Yoki, digita my hello world — vedrai una scheda con «HELLO WORLD». Invio copia.

Passo 5: E poi?

Cambia mode in "list" — restituisci più elementi scorrevoli
Aggiungi actions — pulsanti per copiare, aprire URL, eseguire comandi
Aggiungi metadata — coppie chiave-valore nella barra laterale
Usa background mode — per azioni senza UI
Pubblica sul marketplace — carica su GitHub, invia a Yoki

Continua a leggere per il riferimento completo dell'SDK.

Riferimento SDK

Ogni funzione dell'SDK con esempi reali. Nessuna installazione necessaria.

Import
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

Protocollo v2

Quando l'utente digita la parola chiave, Yoki avvia lo script, scrive V2Input JSON su stdin e legge V2Response da stdout. Finestra nascosta, timeout 5 secondi (configurabile).

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!" }
  ]
}

Campi V2Input

versionSempre "2.0"
commandQuale comando è stato attivato
action"search" (digitazione), "execute" (Invio), "refresh" (auto-refresh)
queryInput completo dell'utente dopo la parola chiave
context.yoki_versionVersione dell'host
context.osSempre "windows" per ora
context.localeLingua dell'utente
context.plugin_dirPercorso assoluto alla cartella del plugin
context.data_dirDirectory dati isolata (persiste tra le esecuzioni)
context.credentialsChiavi dal servizio credenziali (o vuoto)

Modalità risposta

Il campo type decide come Yoki mostra la risposta.

list
PREDEFINITO

Elementi scorrevoli con icona, titolo, sottotitolo e azioni.

detail

Scheda HTML con barra laterale metadati e pulsanti azione. Invio copia la prima azione copy.

grid

Griglia visiva con colonne configurabili.

background
EFFETTO COLLATERALE

Nessuna UI — effetto collaterale con toast HUD. Attivato da Invio.

form
IN ARRIVO

Modulo di input. In arrivo.

error

Errore con dettagli opzionali. I crash dei plugin vengono catturati automaticamente.

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" }))

Permessi

I plugin dichiarano ciò di cui hanno bisogno. Yoki mostra un dialogo di consenso.

network
Whitelist hostname per HTTPS — solo i domini indicati.
filesystem
Percorsi lettura/scrittura relativi alla directory dati. Isolato.
notifications
Notifiche toast Windows.
clipboard
Lettura/scrittura degli appunti di sistema.

Credenziali

I plugin non devono mai includere chiavi API. Dichiara ciò che ti serve.

dichiarazione nel manifesto
{
  "name": "Spotify",
  "keyword": "sp",
  "protocol": "v2",
  "credentials": {
    "slug": "spotify",
    "service": "spotify",
    "keys": ["client_id"]
  },
  "commands": [...]
}
il plugin legge dal contesto
# 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 ...
Invia un PR o scrivi a yokirun@yoki.run.

Riferimento manifesto

Tutti i campi che Yoki capisce. $schema per l'autocompletamento.

plugin.json — schema 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"
    }
  ]
}

Tipi di azione

Imposta action.type su pulsanti o azioni degli elementi:

copy  valueCopia il valore negli appunti.
open_url  urlApri URL nel browser predefinito.
yoki_run  valueEsegui una query Yoki.
exec  exec, argsEsegui un altro script del plugin.
paste  valueIncolla testo nella finestra attiva.
notification  title, bodyMostra notifica di sistema.
close  Chiudi il launcher Yoki.
refresh  Riesegui il comando corrente.

Icone azione

Tre forme per il campo icon:

<svg ...>...</svg> SVG inline, pulito con DOMPurify.
data:image/svg+xml;base64,... Data URL o https:// URL — renderizzato come <img>.
"file" / "folder" / "music" / ... Nome dal set di icone integrato di Yoki.

Scoperta plugin

Yoki scansiona ~/yoki/plugins/ all'avvio e al focus della finestra.
Ogni sottodirectory con plugin.json è un plugin.
I plugin hanno una directory data/ isolata.
I fallimenti di caricamento sono mostrati nel Gestore Plugin.
Le parole chiave duplicate vengono rifiutate.

Test locale

# Testa qualsiasi plugin senza Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.js

Esempi

Plugin ufficiali con il JavaScript SDK. Clona uno come punto di partenza.

Spotify · Node.js · sp

Controllo completo di Spotify — riproduzione attuale, ricerca, dispositivi, playlist.

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
Funzionalità
  • OAuth 2.0 PKCE in pure stdlib
  • Auto-refresh detail card
  • Inline SVG icons
  • Credentials service integration
Extended Math · Node.js · math

Calcolatrice avanzata — trigonometria, integrali, derivate, grafici 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
Funzionalità
  • Safe expression parser (no eval)
  • Symbolic differentiation (chain rule)
  • Numerical integration (Simpson)
  • ASCII function plots
Bookmarks · Node.js · bm

Ricerca istantanea dei segnalibri Chrome, Edge, Brave, Vivaldi.

bm  List all bookmarks
bm github  Search bookmarks
Funzionalità
  • Reads local bookmark files (no API)
  • Multi-browser detection
  • Multi-word fuzzy search
  • Open in browser or copy URL

Prossimi passi

Il sistema plugin è pronto per la produzione.

COMPLETATO
v2 protocol + 5 response modes
stdin/stdout JSON, tutti i modi di risposta, pulsanti azione, auto-refresh.
COMPLETATO
Bundled JavaScript SDK
SDK senza installazione integrati nel binario Yoki.
COMPLETATO
Plugin Marketplace
Esplorazione in-app + installazione in un clic.
COMPLETATO
Credentials service
I plugin dichiarano le necessità, Yoki recupera i segreti a runtime.
COMPLETATO
Scaffold CLI (npx create-yoki-plugin)
Scaffold interattivo — esegui npx create-yoki-plugin, plugin JavaScript funzionante in secondi.
COMPLETATO
JSON Schema for plugin.json
Autocompletamento IDE per tutti i campi del manifesto.
PROSSIMO
Plugin settings UI (form mode)
I plugin dichiarano campi di impostazioni, Yoki renderizza una pagina di impostazioni.
PROSSIMO
Auto-update for plugins
Confronto versione con tag marketplace, badge aggiornamento.
DOPO
AI tool integration
I plugin si espongono come strumenti che Yoki AI può chiamare.
Crea il tuo primo plugin
SDK stabile, tre plugin di riferimento.
Inizia
Yoki Plugin SDK · v2 · yoki.run