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-pluginOppure usa la CLI per generare tutto automaticamente:
npx create-yoki-pluginGenera plugin.json + main.js pronto all'usoPasso 2: Aggiungi plugin.json
Questo file descrive il tuo plugin — nome, parola chiave e script da eseguire.
{
"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
}
]
}Passo 3: Scrivi il tuo script
Lo script legge l'input da Yoki, lo elabora e restituisce una risposta.
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.jsDovresti 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?
Continua a leggere per il riferimento completo dell'SDK.
Riferimento SDK
Ogni funzione dell'SDK con esempi reali. Nessuna installazione necessaria.
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 HTMLProtocollo 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).
{
"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!" }
]
}Campi V2Input
Modalità risposta
Il campo type decide come Yoki mostra la risposta.
Elementi scorrevoli con icona, titolo, sottotitolo e azioni.
Scheda HTML con barra laterale metadati e pulsanti azione. Invio copia la prima azione copy.
Griglia visiva con colonne configurabili.
Nessuna UI — effetto collaterale con toast HUD. Attivato da Invio.
Modulo di input. In arrivo.
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.
Credenziali
I plugin non devono mai includere chiavi API. Dichiara ciò che ti serve.
{
"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 ...Riferimento manifesto
Tutti i campi che Yoki capisce. $schema per l'autocompletamento.
{
"$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:
Icone azione
Tre forme per il campo icon:
Scoperta plugin
Test locale
# Testa qualsiasi plugin senza Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.jsEsempi
Plugin ufficiali con il JavaScript SDK. Clona uno come punto di partenza.
spControllo completo di Spotify — riproduzione attuale, ricerca, dispositivi, playlist.
- OAuth 2.0 PKCE in pure stdlib
- Auto-refresh detail card
- Inline SVG icons
- Credentials service integration
mathCalcolatrice avanzata — trigonometria, integrali, derivate, grafici ASCII.
- Safe expression parser (no eval)
- Symbolic differentiation (chain rule)
- Numerical integration (Simpson)
- ASCII function plots
bmRicerca istantanea dei segnalibri Chrome, Edge, Brave, Vivaldi.
- 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.