Yoki Plugin SDK

Build a plugin in 5 minutes

Everything runs on JavaScript — the SDK is bundled with Yoki, no npm install needed. This guide walks you through creating your first plugin from scratch.

Step 1: Create a folder

Every plugin lives in its own folder inside ~/yoki/plugins/. Create one:

mkdir ~/yoki/plugins/my-plugin

Or use the scaffold CLI to generate everything automatically:

npx create-yoki-pluginGenerates plugin.json + main.js ready to go

Step 2: Add plugin.json

This file tells Yoki about your plugin — its name, trigger keyword, and what scripts to run.

~/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
    }
  ]
}
keywordwhat the user types to trigger your plugin
modehow to show the result. detail = rich card, list = scrollable items
execwhich script file to run
takes_querytrue = user can type text after the keyword

Step 3: Write your script

Your script reads input from Yoki, does something with it, and sends back a response. Three lines of boilerplate, then your logic.

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

Step 4: Test it

Test without Yoki — just pipe JSON into your script:

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

You should see:

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

Now open Yoki, type my hello world — you'll see a card with "HELLO WORLD". Press Enter to copy.

Step 5: What's next

Change mode to "list" — return multiple items the user can scroll through
Add actions — buttons that copy text, open URLs, or run other commands
Add metadata — key-value pairs shown in a sidebar next to your card
Use background mode — for actions that don't show UI (play/pause, save, toggle)
Publish to marketplace — push to GitHub, submit to Yoki marketplace

Keep reading for the full SDK reference with examples for every function.

SDK Reference

Every function in the Yoki SDK, with real examples. The SDK is bundled with Yoki — just import it, no install needed.

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

Protocol v2

When the user types your keyword, Yoki spawns your script, writes V2Input JSON to stdin, reads a V2Response from stdout. Hidden window, 5-second timeout (configurable), stderr captured for debugging.

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

V2Input fields

versionAlways "2.0"
commandWhich command was triggered (matches commands[].name)
action"search" (typing), "execute" (Enter), "refresh" (auto-refresh)
queryFull user input after the keyword
context.yoki_versionHost version (e.g. "1.0.8.2")
context.osAlways "windows" for now
context.localeUser's language (en, ru, de, ...)
context.plugin_dirAbsolute path to your plugin folder
context.data_dirSandboxed data directory (persists across runs)
context.credentialsKeys from the credentials service (or empty)

Response modes

The type field decides how Yoki renders your response.

list
DEFAULT

Scrollable items with icon, title, subtitle, per-item actions.

detail

Rich HTML card with metadata sidebar and action buttons. Enter copies first copy action.

grid

Column-configurable visual grid. Emoji pickers, color palettes.

background
SIDE-EFFECT

No UI — side-effect with HUD toast. Press-Enter-gated.

form
COMING SOON

Multi-field input form. Coming soon.

error

Error with optional details. Plugin crashes are caught automatically.

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

Plugins declare what they need in the manifest. Yoki shows the user a consent dialog at install time.

network
Hostname whitelist for HTTPS — only domains you list.
filesystem
Read/write paths relative to the plugin's data dir. Sandboxed.
notifications
Send Windows toast notifications via the host's notification system.
clipboard
Read/write the system clipboard.

Credentials

Plugins should never embed API keys. Declare what you need and Yoki fetches them at runtime. Your source stays secret-free.

manifest declaration
{
  "name": "Spotify",
  "keyword": "sp",
  "protocol": "v2",
  "credentials": {
    "slug": "spotify",
    "service": "spotify",
    "keys": ["client_id"]
  },
  "commands": [...]
}
plugin reads from context
# 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 ...
How to add a credential: send a PR or email yokirun@yoki.run with your plugin's slug, the service, and the credential's purpose.

Manifest reference

Every field Yoki understands. Add $schema for IDE autocomplete.

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

Action types

Set action.type on buttons or list-item actions:

copy  valueCopy value to clipboard. Enter triggers the first copy action in detail mode.
open_url  urlOpen URL in default browser.
yoki_run  valueRun a Yoki query as if the user typed it.
exec  exec, argsRun another plugin script with arguments.
paste  valuePaste text into the active window.
notification  title, bodyShow a system toast notification.
close  Close the Yoki launcher.
refresh  Re-run the current command (manual refresh).

Action icons

Three forms for the icon field:

<svg ...>...</svg> Inline SVG markup, DOMPurify-sanitized. Use for custom icons.
data:image/svg+xml;base64,... Data URL or https:// URL — rendered as <img>.
"file" / "folder" / "music" / ... Name from Yoki's built-in icon set.

Plugin discovery

Yoki scans ~/yoki/plugins/ on startup and on window focus (hot reload).
Each subdirectory with plugin.json is one plugin.
Plugins get a sandboxed data/ directory for persistent storage.
Load failures are shown in the Plugins Manager — never silent.
Duplicate keywords are rejected — first plugin loaded wins.

Testing locally

# Test any plugin without Yoki running
echo '{"query":"test","command":"main","context":{}}' | node main.js

Showcase

Official plugins built with the JavaScript SDK. Clone any as a starting point.

Spotify · Node.js · sp

Full Spotify control — now playing, search, playback, devices, playlists. OAuth 2.0 PKCE, zero dependencies.

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

Advanced calculator — trig, integrals, derivatives, ASCII plots, constants reference.

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
Features
  • Safe expression parser (no eval)
  • Symbolic differentiation (chain rule)
  • Numerical integration (Simpson)
  • ASCII function plots
Bookmarks · Node.js · bm

Search Chrome, Edge, Brave, Vivaldi bookmarks instantly. Multi-browser, multi-profile.

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

What's next

The plugin system is production-ready. Here's what's shipped and what's coming.

SHIPPED
v2 protocol + 5 response modes
stdin/stdout JSON, detail/list/grid/background/error, action buttons, auto-refresh, hot reload.
SHIPPED
Bundled JavaScript SDK
Zero-install SDKs embedded in the Yoki binary. Import and go.
SHIPPED
Plugin Marketplace
In-app browse + one-click install. Curated review for approved plugins.
SHIPPED
Credentials service
Plugins declare what they need, Yoki fetches secrets at runtime. No embedded API keys.
SHIPPED
Scaffold CLI (npx create-yoki-plugin)
Interactive scaffold — run npx create-yoki-plugin, get a working JavaScript plugin in seconds.
SHIPPED
JSON Schema for plugin.json
IDE autocomplete for all manifest fields. Add $schema to your plugin.json.
NEXT
Plugin settings UI (form mode)
Plugins declare settings fields, Yoki renders a settings page.
NEXT
Auto-update for plugins
Compare installed version with marketplace tag, show update badge, one-click update.
LATER
AI tool integration
Plugins expose themselves as tools the Yoki AI can call directly.
Build your first plugin
The SDK is stable, three reference plugins are live, and the marketplace is open. Start building now.
Get started
Yoki Plugin SDK · v2 · yoki.run