Перейти к содержимому

Ваш первый плагин

Создайте плагин через bb plugin new, установите его в bb, используйте в трёх местах и измените, пока bb его перезагружает. Шаблон плагина — это работающий список задач с одним хранилищем и тремя способами доступа: страница на боковой панели, команда bb hello и скилл, который указывает агенту использовать эту команду. Каждая часть соответствует разделу Как устроен плагин, который, как предполагается, вы уже прочитали.

Потребуется запущенный bb 0.43 и npm. Держите открытым макет зон, чтобы видеть, где в окне располагается каждая поверхность.

Окно терминала
$ bb plugin new hello
Created bb-plugin-hello/ (bb-plugin-hello).
Installed dependencies (npm install).
Next steps:
cd bb-plugin-hello
bb plugin install .

Команда сама устанавливает зависимости. Если этот шаг завершается ошибкой, в выводе среди следующих действий будет npm install --include=dev; выполните её до шага 5.

Команда не принимает флагов. Пакет называется bb-plugin-hello, и bb получает id плагина hello из этого имени, отбрасывая префикс bb-plugin-. Id появляется в URL страницы, в пути к хранилищу и как имя команды CLI.

Шаблон плагина содержит четыре файла, составляющие плагин, и три каталога UI-кода, которым вы управляете.

  • package.json — манифест: идентификатор npm плюс блок bb, который указывает server.ts и app.tsx как две точки входа.
  • server.ts — серверная точка входа: хранилище задач, четыре метода RPC и команда bb hello.
  • app.tsx — клиентская точка входа: страница «Example todos» на боковой панели.
  • skills/example-todos/SKILL.md говорит агенту, какие команды bb hello существуют и когда их использовать.
  • components/ui/, hooks/ и lib/ содержат вендорные компоненты shadcn и хелперы, которые вы можете менять.

Каждый сгенерированный файл, поля манифеста и правило зависимостей описаны в Анатомии пакета.

Откройте server.ts. Без лишних деталей он делает следующее:

// server.ts, abridged
export const rpcContract = defineRpcContract({
todos_list: { input: z.null(), output: z.object({ todos: z.array(todoSchema) }) },
todos_add: { input: z.object({ title: z.string().trim().min(1).max(200) }), output: todoSchema },
// todos_set_done, todos_remove
});
export default async function plugin(bb: BbPluginApi) {
const writeTodos = async (todos: Todo[]) => {
await bb.storage.kv.set("todos", todos);
bb.realtime.publish("todos-changed", { count: todos.length });
};
bb.rpc.register(rpcContract, { todos_list: …, todos_add: …, /* … */ });
bb.cli.register({ name: "hello", commands: [/* list, add, done, undo, remove */], async run(argv) { … } });
bb.onDispose(() => { bb.log.info("disposed"); });
}

Экспорт по умолчанию — это фабрика из Как устроен плагин. Она регистрирует четыре метода RPC и одну команду CLI, и оба вызывают одни и те же хелперы поверх bb.storage.kv. Каждая запись заканчивается вызовом bb.realtime.publish, который сообщает всем открытым страницам об изменении списка. Контракт экспортируется, чтобы app.tsx мог импортировать его тип.

Откройте app.tsx. Страница запрашивает список у сервера по RPC и повторяет запрос при каждом сигнале:

// app.tsx, abridged
function useTodos() {
const rpc = useRpc<typeof rpcContract>(); // typed by the server's contract
// rpc.call("todos_list") on mount, and again on every "todos-changed" signal
useRealtime("todos-changed", refetch);
…
}
function TodosPage() {
const { rpc, todos, refetch } = useTodos();
…
}
export default definePluginApp((app) => {
app.slots.navPanel({
id: "example-todos", title: "Example todos", icon: "ListTodo",
path: "example-todos", component: TodosPage, // route /plugins/hello/example-todos
});
});

app.slots.navPanel добавляет строку в боковую панель bb и выделяет плагину целую страницу. Файл импортирует только тип rpcContract, поэтому серверный код не попадает в бандл браузера. React и SDK тоже не собираются в бандл: bb поставляет их в рантайме, поэтому app.tsx работает только внутри bb.

5. Установите плагин и используйте его из трёх мест

Заголовок раздела «5. Установите плагин и используйте его из трёх мест»
Окно терминала
cd bb-plugin-hello
bb plugin install . # a path install: bb loads server.ts directly and builds only app.tsx
bb plugin dev # leave running: rebuilds and reloads on every save

bb plugin install предупреждает, что плагины выполняются с полным доверием, и просит подтверждения; ответьте y. Без терминала для подтверждения команда откажется работать, если не передать --yes. После установки bb plugin list покажет hello@0.1.0 running.

Теперь проверьте плагин тремя способами, которые настраивает шаблон:

  1. В bb откройте Example todos на боковой панели и добавьте задачу.
  2. В терминале запустите bb hello add "Ship it". Страница покажет новую задачу без перезагрузки, потому что запись команды опубликовала todos-changed.
  3. В треде попросите агента добавить задачу. Скилл велит ему выполнить bb hello add, поэтому изменение пойдёт по тому же пути, что и команда в терминале.

app.tsx · navPanel “Example todos”

/plugins/hello/example-todos

rpc.call("todos_add", …)

useRealtime("todos-changed", refetch)

1↓ rpc.call(…) → POST /api/v1/plugins/hello/rpc/<method>

POST /api/v1/plugins/hello/rpc/<method>

input validated by the schema

{ ok:true, result } | { ok:false, error }

↓ server.ts — jiti, same process

server.ts — jiti, same process

bb.settings.define({ showDone })

bb.rpc.register(rpcContract, …)

bb.cli.register({ name: "hello" })

bb.realtime.publish("todos-changed")

bb.onDispose(() => …)

↓ read and write → bb.storage.kv → plugin_kv in bb.db

4↓ publish to every client → WebSocket /ws — { type:"plugin-signal", pluginId:"hello", channel:"todos-changed", payload } (signal back)

bb hello add "Ship it"

run() executes on the server, not in the CLI

2↓ CLI → server → POST /api/v1/plugins/hello/cli

POST /api/v1/plugins/hello/cli

{ argv, cwd?, threadId? }

↓ server.ts — jiti, same process

the agent in the thread

skills/example-todos/SKILL.md is injected into the thread

3↓ the same command → the same bb hello command

the same bb hello command

the agent never reaches the store directly

↓ server.ts — jiti, same process

bb.storage.kv → plugin_kv in bb.db

key "todos", JSON ≤ 256 KB

beside it: <dataDir>/plugins/hello/

WebSocket /ws — { type:"plugin-signal", pluginId:"hello", channel:"todos-changed", payload }

ephemeral: nothing is stored and nothing is replayed; the client does the filtering, so this is not a confidentiality boundary

↓ every page refetches → app.tsx · navPanel “Example todos” (signal back)

1 the page over RPC · 2 the CLI · 3 the agent running the same command · 4 the signal back to every client. State lives only on the server: the page, the CLI and the agent are three clients of one server.ts.

Все три пути заканчиваются в одном server.ts, и ни один из них не обращается к хранилищу напрямую. bb plugin logs hello -f показывает логи плагина, а bb plugin list показывает его статус.

Добавьте команду count. В server.ts добавьте запись в список commands и case в switch внутри run:

commands: [ /* … */ { name: "count", summary: "Count todos", usage: "bb hello count" } ],
case "count": {
const todos = await listTodos();
return { exitCode: 0, stdout: String(todos.length) };
}

Сохраните файл. bb plugin dev выведет reloaded hello, а bb hello count напечатает число. Теперь bb plugin logs hello показывает loaded от нового экземпляра перед disposed от старого: это порядок перезагрузки из Как устроен плагин.

bb считывает список commands, не запуская ваш код, и добавляет bb hello count в скилл, который генерирует для команд плагина. Два текста вам нужно обновить вручную: строку usage в server.ts, которую выводит bb hello --help, и таблицу команд в skills/example-todos/SKILL.md — скилле, который агент загружает для этого плагина.

СимптомПричинаРешение
@get-bb/plugin-sdk не резолвитсятипы SDK не установленызапустите npm install --include=dev; внутри монорепозитория bb выполните pnpm exec turbo run build:types --filter=@get-bb/plugin-sdk
порталы, тосты или фокус ведут себя страннопакет с заглушкой (shimmed), такой как React или radix, находится в dependencies, поэтому в бандл попала вторая копияперенесите каждый пакет из списка RUNTIME_SLOT_BY_SPECIFIER в devDependencies; zod остаётся в dependencies
плагин остаётся в статусе needs-configurationплагин вызвал bb.status.needsConfiguration(...), либо сервис завершился с NeedsConfigurationErrorисправьте конфигурацию, затем запустите bb plugin reload hello
версия SDK старше, чем запущенный bbшаблон плагина закрепил версию SDK того bb, который его создалзапустите bb plugin types; bb plugin types --check делает ту же проверку в CI без записи
  • Чтобы решить, где ваша идея появится в окне или в агенте, прочитайте Выбор поверхности.
  • Чтобы сохранить секрет, объявите настройку с secret: true; её значение никогда не попадёт в клиентскую точку входа. Подробности — в карточке bb.settings.
  • Чтобы отрендерить компонент внутри ответа агента, посмотрите слот messageDirective.
  • Чтобы задать пользователю вопрос из CLI или инструмента, изучите bb.ui, слот pendingInteraction и паттерн propose-and-confirm в Модели доверия.