Yoki Plugin SDK

Создай плагин за 5 минут

Всё работает на JavaScript — SDK встроен в Yoki, npm install не нужен. Этот гайд проведёт тебя через создание первого плагина с нуля.

Шаг 1: Создай папку

Каждый плагин живёт в своей папке внутри ~/yoki/plugins/. Создай одну:

mkdir ~/yoki/plugins/my-plugin

Или используй CLI для автоматической генерации:

npx create-yoki-pluginГенерирует plugin.json + main.js готовый к работе

Шаг 2: Добавь plugin.json

Этот файл рассказывает Yoki о плагине — его имя, ключевое слово и какие скрипты запускать.

~/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
    }
  ]
}
keywordчто пользователь вводит для вызова плагина
modeкак показать результат. detail = карточка, list = список
execкакой файл скрипта запускать
takes_querytrue = пользователь может вводить текст после ключевого слова

Шаг 3: Напиши скрипт

Скрипт читает ввод от Yoki, обрабатывает его и отправляет ответ. Три строки шаблона, затем твоя логика.

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

Шаг 4: Протестируй

Тест без Yoki — просто передай JSON в скрипт:

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

Ты увидишь:

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

Открой Yoki, набери my hello world — увидишь карточку с «HELLO WORLD». Enter копирует.

Шаг 5: Что дальше

Смени mode на "list" — верни несколько элементов с прокруткой
Добавь actions — кнопки для копирования, открытия URL, запуска команд
Добавь metadata — пары ключ-значение в боковой панели
Используй background mode — для действий без UI (play/pause, сохранение)
Опубликуй в маркетплейс — залей на GitHub, отправь в Yoki marketplace

Читай дальше — полный справочник SDK с примерами для каждой функции.

Справочник SDK

Каждая функция Yoki SDK с реальными примерами. SDK встроен в Yoki — просто импортируй, установка не нужна.

Импорт
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>
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.

Сигнатура
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.

Сигнатура
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.

Сигнатура
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.

Сигнатура
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.

Сигнатура
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").

Сигнатура
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.

Сигнатура
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

Протокол v2

Когда пользователь вводит ключевое слово, Yoki запускает скрипт, записывает V2Input JSON в stdin и читает V2Response из stdout. Скрытое окно, таймаут 5 секунд (настраивается), stderr перехватывается для отладки.

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

versionВсегда "2.0"
commandКакая команда была вызвана (совпадает с commands[].name)
action"search" (набор), "execute" (Enter), "refresh" (автообновление)
queryПолный ввод пользователя после ключевого слова
context.yoki_versionВерсия хоста (напр. "1.0.8.2")
context.osПока всегда "windows"
context.localeЯзык пользователя (en, ru, de, ...)
context.plugin_dirАбсолютный путь к папке плагина
context.data_dirИзолированная папка данных (сохраняется между запусками)
context.credentialsКлючи от сервиса учётных данных (или пусто)

Режимы ответов

Поле type определяет как Yoki отображает ответ.

list
ПО УМОЛЧАНИЮ

Прокручиваемый список элементов с иконкой, заголовком, описанием и действиями.

detail

Карточка с HTML, метаданными в боковой панели и кнопками действий. Enter копирует первое действие copy.

grid

Визуальная сетка с настраиваемыми колонками. Эмодзи, цветовые палитры.

background
ПОБОЧНЫЙ ЭФФЕКТ

Без UI — побочный эффект с HUD-уведомлением. Активируется по Enter.

form
СКОРО

Форма с несколькими полями. Скоро.

error

Ошибка с необязательными деталями. Падения плагинов перехватываются автоматически.

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

Разрешения

Плагины объявляют что им нужно в манифесте. Yoki показывает диалог согласия при установке.

network
Белый список хостов для HTTPS — только указанные домены.
filesystem
Чтение/запись путей относительно папки данных плагина. Изолировано.
notifications
Отправка Windows-уведомлений через систему хоста.
clipboard
Чтение/запись системного буфера обмена.

Учётные данные

Плагины никогда не должны хранить API-ключи. Объяви что нужно, и Yoki получит их в рантайме.

объявление в манифесте
{
  "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 ...
Как добавить учётные данные: отправь PR или напиши yokirun@yoki.run с slug плагина, сервисом и назначением.

Справочник манифеста

Все поля которые понимает Yoki. Добавь $schema для автодополнения в IDE.

plugin.json — полная схема
{
  "$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.type для кнопок или действий элементов списка:

copy  valueСкопировать значение в буфер обмена. Enter активирует первое действие copy в режиме detail.
open_url  urlОткрыть URL в браузере по умолчанию.
yoki_run  valueВыполнить запрос Yoki как если бы пользователь его ввёл.
exec  exec, argsЗапустить другой скрипт плагина с аргументами.
paste  valueВставить текст в активное окно.
notification  title, bodyПоказать системное всплывающее уведомление.
close  Закрыть лаунчер Yoki.
refresh  Перезапустить текущую команду (ручное обновление).

Иконки действий

Три формы для поля icon:

<svg ...>...</svg> Встроенная SVG-разметка, очищенная DOMPurify. Для кастомных иконок.
data:image/svg+xml;base64,... Data URL или https:// URL — рендерится как <img>.
"file" / "folder" / "music" / ... Имя из встроенного набора иконок Yoki.

Обнаружение плагинов

Yoki сканирует ~/yoki/plugins/ при запуске и при фокусе окна (горячая перезагрузка).
Каждая подпапка с plugin.json — это один плагин.
Плагины получают изолированную папку data/ для хранения данных.
Ошибки загрузки показываются в Менеджере плагинов — без молчания.
Дублирующие ключевые слова отклоняются — побеждает первый загруженный плагин.

Локальное тестирование

# Тестируйте любой плагин без запуска Yoki
echo '{"query":"test","command":"main","context":{}}' | node main.js

Примеры плагинов

Официальные плагины на JavaScript SDK. Клонируй любой как стартовую точку.

Spotify · Node.js · sp

Полное управление Spotify — текущий трек, поиск, воспроизведение, устройства, плейлисты. OAuth 2.0 PKCE, ноль зависимостей.

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
Возможности
  • OAuth 2.0 PKCE in pure stdlib
  • Auto-refresh detail card
  • Inline SVG icons
  • Credentials service integration
Extended Math · Node.js · math

Продвинутый калькулятор — тригонометрия, интегралы, производные, 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
Возможности
  • Safe expression parser (no eval)
  • Symbolic differentiation (chain rule)
  • Numerical integration (Simpson)
  • ASCII function plots
Bookmarks · Node.js · bm

Мгновенный поиск закладок Chrome, Edge, Brave, Vivaldi. Мультибраузер, мультипрофиль.

bm  List all bookmarks
bm github  Search bookmarks
Возможности
  • Reads local bookmark files (no API)
  • Multi-browser detection
  • Multi-word fuzzy search
  • Open in browser or copy URL

Что дальше

Система плагинов готова к продакшену. Вот что сделано и что планируется.

ГОТОВО
v2 protocol + 5 response modes
stdin/stdout JSON, detail/list/grid/background/error, кнопки действий, автообновление, горячая перезагрузка.
ГОТОВО
Bundled JavaScript SDK
SDK без установки, встроенные в бинарник Yoki. Импортируй и работай.
ГОТОВО
Plugin Marketplace
Просмотр в приложении + установка в один клик. Модерация одобренных плагинов.
ГОТОВО
Credentials service
Плагины объявляют что им нужно, Yoki получает секреты в рантайме. Без встроенных API-ключей.
ГОТОВО
Scaffold CLI (npx create-yoki-plugin)
Интерактивный scaffold — запусти npx create-yoki-plugin, получи рабочий JavaScript-плагин за секунды.
ГОТОВО
JSON Schema for plugin.json
Автодополнение в IDE для всех полей манифеста. Добавь $schema в plugin.json.
ДАЛЕЕ
Plugin settings UI (form mode)
Плагины объявляют поля настроек, Yoki рендерит страницу настроек.
ДАЛЕЕ
Auto-update for plugins
Сравнение установленной версии с тегом маркетплейса, бейдж обновления, обновление в один клик.
ПОТОМ
AI tool integration
Плагины становятся инструментами, которые Yoki AI может вызывать напрямую.
Создай свой первый плагин
SDK стабилен, три референс-плагина работают, маркетплейс открыт. Начни сейчас.
Начать
Yoki Plugin SDK · v2 · yoki.run