Yoki Plugin SDK

Crée un plugin en 5 minutes

Tout fonctionne en JavaScript — le SDK est intégré à Yoki, pas de npm install. Ce guide te montre comment créer ton premier plugin de zéro.

Étape 1 : Crée un dossier

Chaque plugin a son propre dossier dans ~/yoki/plugins/ :

mkdir ~/yoki/plugins/my-plugin

Ou utilise la CLI pour tout générer automatiquement :

npx create-yoki-pluginGénère plugin.json + main.js prêt à utiliser

Étape 2 : Ajoute plugin.json

Ce fichier décrit ton plugin — nom, mot-clé et scripts à exécuter.

~/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
    }
  ]
}
keywordce que l'utilisateur tape pour déclencher ton plugin
modecomment afficher le résultat. detail = carte, list = liste
execquel fichier de script exécuter
takes_querytrue = l'utilisateur peut taper du texte après le mot-clé

Étape 3 : Écris ton script

Ton script lit les entrées de Yoki, les traite et renvoie une réponse.

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

Étape 4 : Teste

Teste sans Yoki — passe du JSON à ton script :

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

Tu devrais voir :

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

Ouvre Yoki, tape my hello world — tu verras une carte avec « HELLO WORLD ». Entrée copie.

Étape 5 : Et ensuite

Change mode en "list" — retourne plusieurs éléments défilables
Ajoute actions — boutons pour copier, ouvrir des URLs, exécuter des commandes
Ajoute metadata — paires clé-valeur dans la barre latérale
Utilise background mode — pour les actions sans UI
Publie sur le marketplace — pousse sur GitHub, soumets à Yoki

Continue la lecture pour la référence complète du SDK.

Référence SDK

Chaque fonction du SDK avec des exemples concrets. Pas d'installation nécessaire.

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

Signature
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.

Signature
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.

Signature
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.

Signature
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.

Signature
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.

Signature
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").

Signature
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.

Signature
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

Protocole v2

Quand l'utilisateur tape ton mot-clé, Yoki lance ton script, écrit V2Input JSON dans stdin et lit V2Response depuis stdout. Fenêtre cachée, timeout 5 secondes (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!" }
  ]
}

Champs V2Input

versionToujours "2.0"
commandQuelle commande a été déclenchée
action"search" (frappe), "execute" (Entrée), "refresh" (auto-refresh)
queryEntrée complète de l'utilisateur après le mot-clé
context.yoki_versionVersion de l'hôte
context.osToujours "windows" pour le moment
context.localeLangue de l'utilisateur
context.plugin_dirChemin absolu vers le dossier du plugin
context.data_dirRépertoire de données isolé (persiste entre les exécutions)
context.credentialsClés du service de credentials (ou vide)

Modes de réponse

Le champ type détermine comment Yoki affiche ta réponse.

list
PAR DÉFAUT

Éléments défilables avec icône, titre, sous-titre et actions.

detail

Carte HTML avec barre latérale de métadonnées et boutons d'action. Entrée copie la première action copy.

grid

Grille visuelle avec colonnes configurables.

background
EFFET SECONDAIRE

Pas d'UI — effet secondaire avec toast HUD. Déclenché par Entrée.

form
BIENTÔT

Formulaire d'entrée. Bientôt.

error

Erreur avec détails optionnels. Les crashs de plugins sont capturés automatiquement.

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

Permissions

Les plugins déclarent ce dont ils ont besoin. Yoki affiche un dialogue de consentement.

network
Liste blanche d'hôtes HTTPS — uniquement les domaines listés.
filesystem
Chemins de lecture/écriture relatifs au répertoire de données. Isolé.
notifications
Notifications toast Windows.
clipboard
Lecture/écriture du presse-papiers système.

Identifiants

Les plugins ne doivent jamais intégrer de clés API. Déclare ce dont tu as besoin.

déclaration du manifeste
{
  "name": "Spotify",
  "keyword": "sp",
  "protocol": "v2",
  "credentials": {
    "slug": "spotify",
    "service": "spotify",
    "keys": ["client_id"]
  },
  "commands": [...]
}
le plugin lit depuis le contexte
# 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 ...
Envoie un PR ou écris à yokirun@yoki.run.

Référence du manifeste

Tous les champs que Yoki comprend. $schema pour l'autocomplétion.

plugin.json — schéma complet
{
  "$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"
    }
  ]
}

Types d'action

Définir action.type sur les boutons ou actions d'éléments :

copy  valueCopier la valeur dans le presse-papiers.
open_url  urlOuvrir l'URL dans le navigateur par défaut.
yoki_run  valueExécuter une requête Yoki.
exec  exec, argsExécuter un autre script de plugin.
paste  valueColler du texte dans la fenêtre active.
notification  title, bodyAfficher une notification système.
close  Fermer le lanceur Yoki.
refresh  Relancer la commande actuelle.

Icônes d'action

Trois formes pour le champ icon :

<svg ...>...</svg> SVG inline, nettoyé par DOMPurify.
data:image/svg+xml;base64,... Data URL ou https:// URL — rendu en <img>.
"file" / "folder" / "music" / ... Nom du jeu d'icônes intégré de Yoki.

Découverte des plugins

Yoki scanne ~/yoki/plugins/ au démarrage et au focus de la fenêtre.
Chaque sous-répertoire avec plugin.json est un plugin.
Les plugins ont un répertoire data/ isolé.
Les échecs de chargement sont affichés dans le Gestionnaire de Plugins.
Les mots-clés dupliqués sont rejetés.

Test local

# Teste n'importe quel plugin sans Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.js

Exemples

Plugins officiels avec le SDK JavaScript. Clone n'importe lequel comme point de départ.

Spotify · Node.js · sp

Contrôle total de Spotify — lecture en cours, recherche, appareils, 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
Fonctionnalités
  • OAuth 2.0 PKCE in pure stdlib
  • Auto-refresh detail card
  • Inline SVG icons
  • Credentials service integration
Extended Math · Node.js · math

Calculatrice avancée — trigonométrie, intégrales, dérivées, graphiques 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
Fonctionnalités
  • Safe expression parser (no eval)
  • Symbolic differentiation (chain rule)
  • Numerical integration (Simpson)
  • ASCII function plots
Bookmarks · Node.js · bm

Recherche instantanée de favoris Chrome, Edge, Brave, Vivaldi.

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

Et ensuite

Le système de plugins est prêt pour la production.

LIVRÉ
v2 protocol + 5 response modes
stdin/stdout JSON, tous les modes de réponse, boutons d'action, auto-refresh.
LIVRÉ
Bundled JavaScript SDK
SDKs sans installation intégrés dans le binaire Yoki.
LIVRÉ
Plugin Marketplace
Exploration in-app + installation en un clic. Revue curée.
LIVRÉ
Credentials service
Les plugins déclarent leurs besoins, Yoki récupère les secrets à l'exécution.
LIVRÉ
Scaffold CLI (npx create-yoki-plugin)
Scaffold interactif — lance npx create-yoki-plugin, plugin JavaScript fonctionnel en secondes.
LIVRÉ
JSON Schema for plugin.json
Autocomplétion IDE pour tous les champs du manifeste.
SUIVANT
Plugin settings UI (form mode)
Les plugins déclarent des champs de paramètres, Yoki rend une page de paramètres.
SUIVANT
Auto-update for plugins
Comparer la version installée avec le tag marketplace, badge de mise à jour.
PLUS TARD
AI tool integration
Les plugins s'exposent comme outils que Yoki AI peut appeler.
Crée ton premier plugin
SDK stable, trois plugins de référence.
Commencer
Yoki Plugin SDK · v2 · yoki.run