Пользовательские расширения
Расширение — это модуль на TypeScript или JavaScript, который добавляет агенту свой инструмент, команду /, флаг запуска или хук на событие. Расширение подключается со следующего запуска.
Опишите в чате, что нужно: «Сделай расширение, которое блокирует команды с rm -rf». Агент выберет подходящий механизм, напишет файл в ~/.veai/extensions/, проверит его командой veai extensions check и скажет, как включить.
Когда нужно расширение
Расширение — это произвольный код, который выполняется в каждой сессии каждого проекта. Если задачу решает более лёгкий механизм, используйте его:
| Что нужно | Что использовать |
|---|---|
| Повторно используемая инструкция или сценарий | скилл |
| «Всегда делай X» в проекте или везде | правило |
| Инструмент, который обращается к сервису, API или CLI | MCP-сервер |
Хук на вызов инструмента, новая команда / или инструмент, который работает внутри процесса агента | расширение (эта страница) |
Где лежат расширения
Только в ~/.veai/extensions/. Проектных расширений нет: файл в <проект>/.veai/extensions/ не загружается. Так склонированный репозиторий не может запустить свой код в вашей сессии. Если каталога нет, создайте его.
Что лежит в ~/.veai/extensions/ | Загружается |
|---|---|
foo.ts или foo.js | да, этот файл |
foo/index.ts или foo/index.js | да, файл index |
foo/package.json с "pi": { "extensions": ["./src/main.ts"] } | да, каждый существующий файл из списка; путь за пределами foo/ пропускает расширение |
foo/package.json с пустым pi.extensions или с несуществующими файлами и без index | пропускается: файла входа нет |
foo/skills/, foo/prompts/, foo/themes/ | нет, загружаются только файлы входа |
foo/bar/baz.ts без index или манифеста в foo/ | нет |
Имя с суффиксом .off или .bak, файл или каталог | нет, никогда |
notes.md, foo.mjs | нет |
Путь с символом " | пропускается |
Кандидаты загружаются в порядке путей. Расширение из нескольких файлов кладите в каталог: вспомогательный файл рядом с одиночным расширением не входит в проверку, и правка только в нём не будет замечена. Всё нужное расширению держите внутри его каталога: символьная ссылка за его пределы и каталог .git/ в проверку тоже не входят.
Как написать расширение
Расширение экспортирует функцию по умолчанию, которая получает ExtensionAPI. Функция может быть асинхронной. TypeScript выполняется как есть, сборка не нужна. Пакет typebox и пакеты самого движка доступны без установки.
Инструмент для модели:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "word_count",
label: "Word count",
description: "Count words in a text",
parameters: Type.Object({ text: Type.String() }),
async execute(_toolCallId, { text }) {
// content читает модель, details — структурированные данные для интерфейса и сессии
return { content: [{ type: "text", text: String(text.split(/\s+/).filter(Boolean).length) }], details: {} };
},
});
}
Только команда:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.registerCommand("today", {
description: "Show today's date",
handler: async (_args, ctx) => {
ctx.ui.notify(new Date().toDateString(), "info");
},
});
}
Только хук: блокирует опасную команду до выполнения:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event) => {
if (event.toolName === "bash" && String((event.input as { command?: string }).command ?? "").includes("rm -rf")) {
return { block: true, reason: "rm -rf is blocked by a user extension" };
}
});
}
Полное описание API — все события, ctx, отрисовка, провайдеры — в собственной документации движка. Команда veai extensions check печатает абсолютный путь к ней в строке engine API:.
Зависимости
Сначала используйте встроенные модули Node (node:fs, node:path, node:child_process) и глобальный fetch. Если без пакета не обойтись, сделайте расширение каталогом ~/.veai/extensions/<имя>/ с package.json, где пакет указан в dependencies, и файлом index.ts, а затем выполните npm install в этом каталоге. node_modules/ оттуда подхватывается автоматически.
Расширение, у которого не установлен импортируемый пакет, пропускается с подсказкой выполнить npm install в его каталоге. После установки оно загрузится со следующего запуска без правок файла.
Зарезервированные имена
Расширение не может зарегистрировать инструмент или флаг, имя которых уже занято:
- встроенные инструменты движка:
read,bash,powershell,edit,write,grep,find,ls; - встроенные флаги движка, например
--modelи--extension; - все инструменты и флаги самого Veai CLI, например
bash_output,fork,subagent,use_skill,web_search,--mcp-config; - имя, которое раньше зарегистрировало другое пользовательское расширение.
Расширение с занятым именем пропускается целиком. Полный список зарезервированных имён печатает veai extensions check --verbose в строке reserved:. Встроенный инструмент нельзя переопределить, зарегистрировав то же имя.
С хуками иначе: обработчик на tool_call, tool_result или любое другое событие может наблюдать за любым инструментом и блокировать его, включая встроенные.
Для команд / резервирования нет. Движок оставляет одинаковые команды под именами /имя:1, /имя:2 и скрывает команду, совпавшую со встроенной.
Субагенты, форки и /btw загружают те же расширения, что и основной агент, поэтому хуки действуют во всех дочерних агентах. Если в файле агента задан список tools:, инструмент расширения доступен такому агенту, только если список его называет.
Проверка: veai extensions check
Сразу после создания или правки расширения выполните:
veai extensions check
Команда загружает каждого кандидата так же, как это сделает следующий запуск, но без кэша результатов, и печатает:
| Строка | Что значит |
|---|---|
<файл> loads: tools=… flags=… commands=… handlers=… | расширение принято |
<файл> skipped: <причина> | пропущено, причина указана |
<имя> disabled (settings: extensions.disabled) | отключено в настройках, не проверялось |
no user extensions in ~/.veai/extensions/ | загружать нечего |
user extensions not loaded: the preflight failed: <причина> | сама проверка не смогла выполниться |
last restart: <файл>: <ошибка> | движок не смог загрузить этот файл при прошлом запуске |
<имя>.bak backup present: delete it once the edit is confirmed | остался файл для отката |
reserved: … и engine API: <путь> | зарезервированные имена и путь к документации движка |
Команда завершается с кодом 1, если включённое расширение пропущено или проверка не удалась, иначе с кодом 0.
Строка loads доказывает, что файл загружается и что он регистрирует. Правильно ли работает execute инструмента или хук, видно только при реальном вызове.
Как включить
Расширение подхватывается при запуске. Что нужно сделать после изменения:
| Изменение | Что сделать |
|---|---|
| Новое расширение | Перезапустите агента в том же каталоге проекта командой veai -c: она продолжает последнюю сессию |
| Код расширения, которое уже загружается | /reload в запущенной сессии |
| Имя инструмента или флага | Перезапуск через veai -c, не /reload: /reload проверку не выполняет |
| Отключение или включение расширения | Перезапуск через veai -c |
Если расширение при запуске пропущено, терминал показывает предупреждение с причиной и подсказкой запустить veai extensions check. Загрузка, которая не уложилась в 5 секунд проверки, тоже пропускает расширение: такое расширение замедляло бы каждый запуск.
Правка, откат и отключение
Перед правкой существующего расширения скопируйте его в <имя>.bak (guard.ts.bak или каталог jira.bak/). Если <имя>.bak уже есть, не перезаписывайте его: в нём последняя подтверждённая версия. Имя с суффиксом .bak никогда не загружается.
Затем внесите правку, выполните проверку и включите изменение по таблице выше. Когда работа подтверждена, удалите .bak. Если не работает, верните .bak на место расширения и включите его тем же способом. Если проверка сообщает о забытом <имя>.bak, решите, работает ли последняя правка, и удалите копию или восстановите её. Агент делает то же самое, когда правит расширение по вашей просьбе.
Чтобы отключить расширение, не удаляя его, добавьте его имя в extensions.disabled в ~/.veai/settings.json и перезапустите агента. Имя — это имя файла, например guard.ts, или имя каталога, например jira:
{
"extensions": {
"disabled": ["guard.ts", "jira"]
}
}
Отключить расширение можно и переименованием в имя с суффиксом .off. Настройка действует только на глобальном уровне: проектный settings.json отключить расширение не может. См. «Настройки».
Если движок всё равно не загрузил расширение
Если движок не загрузил расширение, прошедшее проверку, агент один раз перезапускается без пользовательских расширений и называет проблемный файл.
- Не хватает зависимости (
Cannot find module): выполнитеnpm installв каталоге расширения, затем запустите проверку. Править файл не нужно: следующий запуск проверит его заново. - Любая другая ошибка: файл остаётся пропущенным, пока его содержимое не изменится. Исправьте файл и запустите проверку.
Совместимость
Совместимость расширений при обновлении агента или его движка не гарантируется. Расширение, которое сломалось после обновления, пропускается с указанием причины: исправьте его и запустите проверку.