Плагин для OpenCode, который превращает встроенный lsp tool в семантически понятный для LLM интерфейс. Вместо указания точных координат (строка, колонка) модель передаёт имя символа и фрагмент кода, а плагин автоматически определяет позицию. Если LLM при этом передает некорректное имя фрагмента или недостаточно уникальный фрагмент кода, плагин к ответу от LSP подмешивает инструкции, как сделать вызов точнее, это снижает вероятность зацикливания вызова инструмента.
OpenCode предоставляет lsp tool для взаимодействия с LSP-серверами (goToDefinition, findReferences и др.), но требует точных координат символа — line и character. LLM регулярно ошибается при их определении, что приводит к неверным результатам.
Semantic LSP Plugin перехватывает вызовы lsp tool через систему хуков OpenCode Plugin API:
- Подмена интерфейса — убирает параметры
line/character, добавляетsymbol/fragment - Определение координат — читает файл, находит символ через алгоритм text-based matching, подставляет точные
line/characterв вызов - Обогащение ответа — добавляет предупреждения и ошибки, помогающие LLM скорректировать запрос
LLM вызывает lsp(filePath, operation, symbol, fragment)
│
▼
tool.definition → подмена параметров
│
▼
tool.execute.before → чтение файла → поиск символа → подстановка line/character
│
▼
Оригинальный lsp tool
│
▼
tool.execute.after → обогащение ответа ошибками/предупреждениями
│
▼
Обогащённый ответ для LLM
| Операция | Описание |
|---|---|
goToDefinition |
Переход к определению символа |
findReferences |
Поиск всех ссылок на символ |
hover |
Документация и тип символа |
documentSymbol |
Все символы в документе |
workspaceSymbol |
Поиск символов по всей workspace |
goToImplementation |
Поиск реализаций интерфейса |
prepareCallHierarchy |
Иерархия вызовов в позиции |
incomingCalls |
Кто вызывает функцию |
outgoingCalls |
Кого вызывает функция |
Плагин распространяется как npm-пакет opencode-semantic-lsp (внутренний registry: выгрузка
tarball-архива вручную, скриптов публикации в package.json нет). Рантайм-зависимостей пакет не
имеет — zod бандлится esbuild в dist/semantic-lsp-plugin.js.
Активируйте плагин в opencode.json проекта (или глобально в ~/.config/opencode/opencode.json):
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-semantic-lsp"]
}Либо той же командой, которая ещё и допишет поле plugin и установит пакет:
opencode plugin opencode-semantic-lspПин версии — стандартным синтаксисом npm:
{ "plugin": ["opencode-semantic-lsp@1.0.0"] }opencode установит пакет в ~/.cache/opencode/packages/<pkg>/node_modules/<pkg> и загрузит точку
входа exports["./server"] (dist/semantic-lsp-plugin.js). id плагина (semantic-lsp) задаётся в
модуле и от имени пакета не зависит. Совместимость проверяется по engines.opencode (>=1.17).
Независимо от способа установки (npm / dir / ручное копирование) плагин подменяет только интерфейс
lsptool — сами LSP-серверы активирует opencode. Нужны все условия одновременно.
| Требование | Как выполнить |
|---|---|
Экспериментальный lsp tool |
OPENCODE_EXPERIMENTAL_LSP_TOOL=true (или OPENCODE_EXPERIMENTAL=true) в окружении процесса opencode |
| Включённые LSP-серверы | "lsp": true или объект конфигурации LSP (например "lsp": {"clangd": {...}}) в opencode.json |
| Бинарь LSP-сервера | typescript-language-server, clangd и т.п.: opencode скачивает и кэширует их в ~/.cache/opencode; если задан собственный lsp.<id>.command, бинарь обязан быть в PATH |
| Разрешение на tool | "permission": { "lsp": "allow" } |
opencode.json с полным набором требований:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-semantic-lsp"],
"lsp": true,
"permission": {
"lsp": "allow"
}
}Важно:
- флага
OPENCODE_EXPERIMENTAL_LSP_TOOL=trueнедостаточно — он включает сам tool, но не LSP-серверы; - без ключа
lspopencode 1.17.x отключает все встроенные LSP-серверы (в логе —all LSPs are disabled), и любой вызовlspзавершается ошибкойNo LSP server available for this file type.; - проверка логов: при успешной активации присутствует запись
enabled LSP serversс нужнымidсервера.
Для правки/отладки плагина можно собирать артефакты из исходников и подключать их без переупаковки в npm-пакет.
git clone <repo-url>
cd opencode-symbol-definition-finder
npm install
npm run buildРезультат сборки:
dist/semantic-lsp-plugin.js— бандл плагина (esbuild, ESM)dist/symbol-finder.js— Custom Tool для OpenCode (standalone)dist/— скомпилированные JS-файлы библиотеки (tsc)
Создайте .opencode/plugins/semantic-lsp-plugin.js в корне проекта:
mkdir -p .opencode/plugins
cp dist/semantic-lsp-plugin.js .opencode/plugins/Создайте .opencode/package.json с зависимостью от Plugin SDK:
{
"dependencies": {
"@opencode-ai/plugin": "latest"
}
}Установите зависимости:
cd .opencode && npm install && cd ..Обе команды сначала делают «чистую» подготовительную сборку (clean → typecheck →
build:plugin — только бандл плагина и его .d.ts, без skills и рантаймов lib/cli), а затем
упаковывают результат по полю files. Публикационных хуков (prepublishOnly / prepack) в
package.json нет намеренно: пакет выгружается вручную.
npm run pack→opencode-semantic-lsp-<version>.tgzв корне репозитория — артефакт для выгрузки во внутреннюю систему;npm run pack:dir→pkg/opencode-semantic-lsp/— разархивированная директория, структурой идентичная tarball.
.tgz, pkg/ и .pack-tmp/ — выходные артефакты, они в .gitignore.
Дистрибутив контролируется тестами: быстрый integrity-сьют npm test проверяет собранный pkg/opencode-semantic-lsp/ по метаданным и форме модуля, а plugin-e2e (npm run test:e2e) загружают этот же артефакт как модуль через dir-спеку plugin → exports["./server"] (подробнее — в разделе «Тесты»).
Альтернатива npm-tarball'у для «переноса» собранного плагина между машинами/проектами: npm run pack:dir готовит разархивированную директорию pkg/opencode-semantic-lsp/ со структурой,
идентичной tarball (тот же npm pack, состав по files), без самого .tgz на выходе.
npm run pack:dir
# → pkg/opencode-semantic-lsp/ (package.json, README.md, LICENSE,
# CHANGELOG.md, dist/semantic-lsp-plugin.js,
# dist/semantic-lsp-plugin.d.ts)Подключить в проекте-потребителе — два рабочих варианта (оба проверены против opencode 1.18):
-
dir-спекой в
opencode.jsonconsumer'а: скопироватьpkg/opencode-semantic-lsp/целиком в корень проекта-потребителя (получится./pkg/opencode-semantic-lsp/относительно корня consumer'а) и добавить вopencode.jsonconsumer'а:{ "plugin": ["./pkg/opencode-semantic-lsp"] }Относительный путь резолвится относительно корня проекта потребителя, поэтому
pkg/обязан лежать внутри него. Работает и абсолютный путь. -
Ручным копированием: положить
pkg/opencode-semantic-lsp/dist/semantic-lsp-plugin.jsв.opencode/plugins/consumer'а (тот же файл, что и в npm-пакете) — см. подраздел выше.
LLM передаёт параметры через lsp tool:
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
filePath |
string | Да | Путь к файлу |
operation |
string | Да | LSP-операция |
symbol |
string | Да | Имя символа (функция, переменная, класс) |
fragment |
string | Да | Фрагмент кода, содержащий символ (для дизамбигуации) |
src/
├── semantic-lsp-plugin.ts # Plugin для OpenCode — основной продукт
├── semantic-lsp-transformer/ # Ядро алгоритма поиска символов
│ ├── SemanticLspTransformer.ts # Основной класс с find() и bestEffort
│ ├── types.ts # Типы: FinderResult, FinderError, ...
│ ├── formatters/
│ │ ├── jsonFormatter.ts # JSON-форматирование
│ │ ├── llmFormatter.ts # Человекочитаемый формат для LLM
│ │ ├── lspFormatter.ts # Структурированный формат для plugin
│ │ └── formatterFactory.ts # Фабрика форматтеров
│ ├── search/
│ │ └── RegexSearchStrategy.ts # Стратегия поиска на основе regex
│ ├── validation/
│ │ ├── ValidationChain.ts # Цепочка валидаторов
│ │ ├── EmptyCodeValidator.ts # ...
│ │ ├── EmptySymbolValidator.ts # Валидаторы входных данных
│ │ ├── EmptyFragmentValidator.ts # ...
│ │ ├── InvalidSymbolValidator.ts # ...
│ │ └── SymbolInFragmentValidator.ts# ...
│ └── utils/
│ └── textNormalizer.ts # Нормализация текста
├── opencode-tool.ts # Custom Tool (standalone symbol-finder)
├── cli.ts # CLI-утилита
├── index.ts # Публичный API библиотеки
├── infra/
│ ├── fileReader.ts # Интерфейс чтения файлов
│ └── nodeFileReader.ts # Реализация через Node.js fs
└── skills/
└── go-to-definition/SKILL.md # Навык для OpenCode
Не входят в публикационный tarball (см. «Публикационные артефакты»). Доступны только при работе с исходниками (
npm run build— собирает lib/CLI без ограничений поляfiles).
node dist/cli.js -f src/app.ts -s myFunction -F "myFunction(arg1, arg2)"
node dist/cli.js --file code.py --symbol MyClass --fragment "MyClass()" --format llm
node dist/cli.js -f main.go -s handler -F "handler(req)" --best-effortПоля bin в package.json нет: CLI не устанавливается из npm-пакета и запускается только из
собранных исходников репозитория.
// только из исходников репозитория (после `npm run build`); из npm-пакета библиотека не резолвится
import { SemanticLspTransformer, LspFormatter } from "./dist/index.js";
const finder = new SemanticLspTransformer();
const result = finder.find({
code: "const x = foo(1);\nfunction foo(n) { return n; }\nfoo(42);",
symbol: "foo",
fragment: "foo(42);",
});Режим bestEffort: true всегда возвращает ровно одну позицию с fallback на {line: 1, column: 1} при невозможности найти символ.
dist/symbol-finder.js — standalone ESM-бандл, размещаемый в .opencode/tools/. Предоставляет прямой поиск символов без привязки к LSP.
npm test # Unit + Integration + целостность пакета
npm run test:e2e # E2E без clangd (сюита semantic-lsp пропускается)
npm run test:e2e -- --clangd /path/to/clangd # E2E целиком, включая clangd-сюитуnpm test дополнительно прогоняет быстрый не-LLM сьют целостности публикационного пакета (tests/packaging/publish-integrity.test.ts, офлайн): он собирает артефакт через npm run pack:dir и проверяет состав по files, резолв exports["./server"] на существующий файл внутри пакета, форму собранного модуля (default{id, server} + три хука tool.definition/tool.execute.before/tool.execute.after), самодостаточность бандла (только fs/path) и гейт engines.opencode. Для повторного прогона без пересборки — SKIP_PACK_BUILD=1 npm test.
Дистрибутив проверяется загрузкой как модуль (не ручным copy): plugin-e2e подключают плагин через opencode.json: plugin: ["<ABS>/pkg/opencode-semantic-lsp"] → package.json: exports["./server"] → import() собранного бандла (tests/e2e/lsp-tool-definition.test.ts, semantic-lsp.test.ts, lsp-goto-definition.test.ts). Временные consumer-проекты создаются внутри репозитория (.e2e-tmp/, чтобы opencode резолвил node_modules/tsserver), фикстуры не мутируются. Ручное копирование loose-файла сохранено ровно в одном smoke-тесте (tests/e2e/plugins-dir-manual-copy.smoke.test.ts).
E2E-тесты требуют opencode в PATH (или OPENCODE_BIN=/path/to/opencode) и активации lsp tool (см. выше).
Сюита tests/e2e/semantic-lsp.test.ts зависит от бинаря clangd и по умолчанию пропускается — прогон остаётся зелёным, а в конце выводится подсказка, как включить эти тесты. Путь к clangd задаётся только явно через --clangd: никакие поиски в PATH или кэше opencode не выполняются, пропустить сюиту «специальным флагом» нельзя — её просто не запускают.
Прогон запускается скриптом tests/e2e/run-e2e.js в три шага: (1/3) distributable (npm run pack:dir — публикационный артефакт pkg/opencode-semantic-lsp/), (2/3) build dist (npm run build — полный dist/ для manual-copy smoke и tool-ассетов) и (3/3) jest. Порядок pack:dir → build обязателен: pack:dir чистит dist/ и пересобирает только плагин, а build затем восстанавливает полный dist/, не трогая pkg/. Перед запуском jest проверяется наличие pkg/opencode-semantic-lsp/dist/semantic-lsp-plugin.js; в окружение jest пробрасывается E2E_PACKAGE_DIR. Ошибки делятся на два класса:
- некорректный вызов (
--clangdбез значения, повтор флага, значениеauto) — баннерE2E RUN ABORTED, прерывание до сборки, ни один тест не запускается; - проблема с самим бинарём (путь не найден, это не файл,
--versionне запускается, вывод не похож на clangd) — сразу печатается баннерINVALID CLANGD PATH, но прогон продолжается: сюиты, не зависящие от clangd, выполняются как обычно, аsemantic-lsp.test.tsпадает с той же диагностикой, и итоговый код прогона ненулевой.
Вывод сборки при успехе сворачивается в строку build ... ok (1.6s): сообщения esbuild вида «Done in 30ms» иначе читаются как результаты тестов. Полный вывод сборки показывается только когда сборка упала.
Проверенный путь попадает в конфигурацию фикстуры как lsp.clangd.command[0], поэтому clangd не обязан быть в PATH (важно для Windows).
- Node.js
- TypeScript 6+
- LSP-сервер, настроенный для соответствующего языка проекта (clangd для C/C++, tsserver для TS/JS и т.д.)
- Для E2E:
OPCODE_EXPERIMENTAL_LSP_TOOL=true,"lsp": trueв конфигурации фикстуры; clangd-проверки — черезnpm run test:e2e -- --clangd <path>