Як написати плагін

From docs/uk/plugin-authoring.md at cc732c3 View on GitHub

Плагін реалізує один трейт — Plugin з опублікованого крейта rtok-plugin-sdk. Це так незалежно від того, чи постачається плагін у цьому репозиторії, чи живе в чужому (D25): контракт один, і жодного обхідного шляху для вбудованих плагінів немає. crates/rtok-plugin-sdk/examples/shrink.rs — повний плагін в одному файлі; examples/hello_plugin.rs — те саме, підключене до справжнього хоста.

0. Чи має він існувати?

Перегляньте plan.md §1 (каталог) і §0 (рішення), а також roadmap.md щодо порядку задач цього плагіна. Якщо методу там немає, додайте його в ideas.md (вихідний інструмент + плагін; Пізніше, якщо це v0.2+) і спершу запропонуйте зміну плану. Кожен плагін тут пишеться з нуля: він не має викликати через оболонку, лінкувати чи читати дані стороннього інструмента (D6). Інструмент, який він замінює, — це специфікація; research.md — докази.

1. Трейт

use rtok_plugin_sdk::{
    Ctx, DashboardPage, Manifest, Plugin, PreToolDecision, PreToolUse, Surface,
};

pub struct MyPlugin;

impl Plugin for MyPlugin {
    // Обов'язково: що це за плагін.
    fn manifest(&self) -> Manifest {
        Manifest { id: "<id>", surfaces: &[Surface::Hook], default_on: true }
    }

    // Обов'язково: сторінка, яку відображають і `rtok web`, і `rtok tui` (D23).
    fn dashboard_page(&self) -> DashboardPage {
        DashboardPage::new("<Title>", "One sentence for the operator.", true)
    }

    fn pre_tool(&self, ev: &PreToolUse, cx: &Ctx) -> Option<PreToolDecision> {
        let cfg = cx.plugin_config::<crate::config::MySection>("<id>");
        let _ = (ev, cfg);
        None
    }
}

Два обов'язкові методи обов'язкові вже під час компіляції: плагін, який не каже, що він таке і яку сторінку показує, не збирається. Кожен метод подій — pre_tool, post_tool, session_start, prompt_submit, pre_compact, mcp_tools, proxy_filter — має типову реалізацію, що нічого не робить, тож реалізуйте лише інтерфейси, які оголошує маніфест. Документація крейта містить таблицю того, коли хост запитує кожен із них.

cx — це хост: cx.estimate(text, Class::Code), cx.record(&Measurement { .. }), cx.log(..), cx.plugin_config::<T>("<id>") для вашого власного розділу [plugins.<id>] і cx.config::<T>("proxy") для рідкісного значення на рівні всього хоста. До збереженого стану звертаються так само — cx.put_archive(..), cx.search_notes(..), cx.symbol_defs(..) — через трейти можливостей, до яких розіменовується Ctx. Плагін ніколи не звертається до бази даних хоста напряму; якщо якоїсь можливості бракує, її свідомо додають до SDK, а не обходять.

2. Усередині цього репозиторію: підключіть його (три однорядкові зміни)

  • Cargo.toml → [features]: додайте <id> = [] і допишіть його до default.
  • src/plugins/mod.rs: #[cfg(feature = "<id>")] pub mod <id>; і відповідний v.push(Box::new(<id>::MyPlugin)); в all(). Позиція = порядок диспетчеризації.
  • src/config.rs → CATALOGUE: ("<id>", default_on) на тій самій позиції.

Тест registry_matches_catalogue падає, доки всі три не узгоджено. Імпортуйте з rtok_plugin_sdk напряму — crate::plugin реекспортує ті самі типи для власного коду диспетчеризації хоста, але плагін називає крейт, який він реалізує.

3. Поза цим репозиторієм

[dependencies]
rtok-plugin-sdk = "0.0.1"

Реалізуйте Plugin на основі крейта — три залежності, без SQLite, без tree-sitter, без вебсервера. Щоб запустити його, зберіть бінарник, який залежить від rtok і передає ваш плагін реєстру: Registry::from_plugins(vec![Box::new(Mine)], &Config::load()?). Цей репозиторій не постачає сторонніх плагінів і жодних адаптерів. v0.2 (P32) додає необов'язковий шлях для того самого контракту у вигляді .wasm у ~/.rtok/plugins/ лише для rtok mcp / rtok proxy — ніколи для rtok hook; див. crates/rtok-plugin-sdk/PLAN.md § v0.2 survey — WASM plugin host.

4. Документація поруч із кодом

  • src/plugins/<id>/PLAN.md — проєктна нотатка D15 (скопіюйте docs/plugin-plan-template.md). Потрапляє в коміт перед першою задачею реалізації плагіна. Обов'язкові заголовки, ≥ 3 розглянуті альтернативи, один Target: і один Falsified by:. Перевіряється cargo test plugin_plans.
  • src/plugins/<id>/README.md — що він робить, що замінює, інтерфейси, механізм, ключі конфігурації, задачі плану, статус. Для користувачів.
  • src/plugins/<id>/AGENTS.md — файли, якими він володіє, інваріанти, які не можна порушувати, дозволені залежності, Checks. Для того (людини чи моделі), хто редагуватиме його наступним.

Скопіюйте структуру з наявного плагіна; README і AGENTS.md мають уміщатися на один екран.

5. Правила, яких дотримується кожен плагін

  1. Fail open. За будь-якої помилки повертайте None/порожнє значення. Ніколи не панікуйте на шляху хука; диспетчер перехоплює паніки, але паніка все одно є багом.
  2. Без втрат. Якщо ви щось скорочуєте, спершу збережіть оригінал через cx.put_archive і покладіть id у Measurement::ref_id та в текст, який бачить модель. Модель має мати змогу виконати для нього expand.
  3. Вимірюйте. Кожна дія, що змінює те, що бачить модель, викликає cx.record(&Measurement) з байтами до/після й оціненими токенами. Економії без рядка не існує.
  4. Бюджет. Усе для SessionStart/UserPromptSubmit повертається як Injection і ніколи не записується в additionalContext напряму. Текст має бути побайтово стабільним між ходами.
  5. Швидкість. Шлях хука ≤ 10 мс p95 у release-збірці. Один індексований запит до БД — нормально; обхід файлової системи чи мережевий виклик — ні.
  6. Жодної нової залежності без однорядкового обґрунтування в повідомленні коміту, і лише з базового списку в plan.md §2.
  7. Не дублюйте. Повторно використайте наявну допоміжну функцію або винесіть одну спільну на рівень, якому належить ця поведінка.

6. Тестування

Модульні тести живуть у модулі. Поза деревом або для плагіна, якому не потрібне сховище, використовуйте rtok_plugin_sdk::testing::MemoryHost — він зберігає те, що плагін записує й архівує:

use rtok_plugin_sdk::testing::MemoryHost;
use rtok_plugin_sdk::Ctx;

#[test]
fn rewrites_recursive_ls() {
    let host = MemoryHost::new();
    let input = serde_json::json!({ "command": "ls -R src" });
    let ev = PreToolUse { tool_name: "Bash", tool_input: &input };
    assert!(MyPlugin.pre_tool(&ev, &Ctx::new(&host)).is_some());
    assert_eq!(host.recorded().len(), 1);
}

У цьому репозиторії crate::plugin::Runtime::in_memory("test") — це справжній хост без файлу: використовуйте його, коли перевірка стосується того, що насправді потрапило до сховища, а фікстури в tests/fixtures/hooks/ — для реалістичних подій. Потім Check задачі з plan.md (порядок див. у roadmap.md), а потім just check.

7. Випуск

Закомітьте <task-id>: <title> у власній гілці задачі й відкрийте для неї один пул-реквест (D16). У тому самому пул-реквесті позначте задачу виконаною і перенесіть її з plan.md до done.md з датою та виводом Check. Реалізована робота, яка досі значиться в plan.md, незавершена.