Skip to content

maxvst/opencode-ext-search-plugin

Repository files navigation

ext-search — плагин OpenCode для поиска по внешним директориям

Описание

Плагин ext-search расширяет возможности встроенных инструментов grep и glob в OpenCode, позволяя прозрачно для ИИ искать файлы в заранее настроенных внешних директориях монорепозитория.

Проблема

При работе с монорепозиторием в OpenCode возникает дилемма:

  • Если открыть всю монорепу — поиск grep/glob работает по всему дереву, что медленно.
  • Если открыть только подпроект — ИИ не видит файлы из shared-пакетов (типы, утилиты и т.д.).

Плагин решает эту проблему: он перехватывает результаты grep и glob и дополняет их результатами поиска по внешним директориям. Для ИИ всё прозрачно — он продолжает пользоваться теми же инструментами.

Дополнительный инструмент deps_read

Плагин также регистрирует кастомный инструмент deps_read для чтения файлов из внешних директорий. Это удобнее, чем встроенный read, который может требовать подтверждения для каждого файла за пределами проекта.

Требования

  • OpenCode — установлен и доступен в системе (включает Bun)

  • ripgrep (rg) — для поиска по содержимому файлов (grep). Плагин автоматически ищет rg в следующем порядке:

    1. Системный PATH
    2. Стандартные директории OpenCode — плагин формирует все возможные пути путём комбинирования платформенных базовых директорий (XDG cache/data, ~, AppData и др.) со стандартными суффиксами (opencode/bin, .opencode/bin, .cache/opencode/bin, .local/share/opencode/bin, Library/Caches/opencode/bin, Library/Application Support/opencode/bin)

    OpenCode автоматически скачивает rg при первом запуске, поэтому отдельная установка обычно не требуется. Поиск файлов (glob) не зависит от rg — используется нативный Bun.Glob.

    Если rg не найден, плагин продолжает работу: поиск файлов (glob) не затрагивается, а при каждом вызове grep к результатам добавляется подсказка с абсолютными путями внешних директорий, чтобы ИИ мог использовать deps_read или glob для их проверки:

    (ripgrep not available. External dependency directories: /home/user/my-monorepo/shared-types, /home/user/my-monorepo/common-utils.
    Use the deps-read tool or search with glob specifying an external directory path to explore their contents.)
    

Установка

1. Скопируйте плагин в директорию команды

Соберите плагин (npm run build) и поместите артефакты в .opencode/plugins/ext-search/ внутри директории команды:

монорепа/
├── shared-types/
├── common-utils/
└── team-alpha/
    ├── opencode.json
    ├── .opencode/
    │   └── plugins/
    │       └── ext-search/
    │           ├── package.json
    │           └── index.js     ← из plugins/ext-search/dist
    └── my-app/           ← подпроект, открываемый в OpenCode

2. Настройте opencode.json

Добавьте плагин в конфигурацию OpenCode в файле opencode.json в директории команды:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    [
      "./.opencode/plugins/ext-search",
      {
        "root": "../../",
        "directories": [
          "shared-types",
          "common-utils"
        ],
        "excludePatterns": ["node_modules", ".git", "dist", "*.test.*"],
        "maxResults": 50,
        "strict_path_restrictions": false,
        "compile_commands_dir": null
      }
    ]
  ]
}

3. Настройте разрешения для внешних директорий

Для работы встроенного инструмента read с файлами из внешних директорий необходимо задать permission.external_directory. Поскольку эта секция содержит абсолютные пути, специфичные для машины разработчика, её не следует помещать в проектный opencode.json — вместо этого используйте глобальный конфиг OpenCode.

Примечание: плагин автоматически одобряет запросы external_directory для путей внутри настроенных внешних директорий и внутри configDir (см. Авто-permit). Ручная настройка permission.external_directory в глобальном конфиге по-прежнему поддерживается для полноты, но в большинстве случаев авто-permit делает её необязательной.

Путь к глобальному конфигу зависит от платформы:

Платформа Путь
Linux ~/.config/opencode/opencode.json
macOS ~/Library/Application Support/opencode/opencode.json
Windows %APPDATA%\opencode\opencode.json

Пример глобального конфига:

{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "external_directory": {
      "/абсолютный/путь/к/монорепе/shared-types/*": "allow",
      "/абсолютный/путь/к/монорепе/common-utils/*": "allow"
    }
  }
}

Для каждой внешней директории укажите абсолютный путь с glob-суффиксом /*.

Параметры конфигурации

Параметр Тип Обязательный Описание
root string Нет Путь к корню монорепы (относительно открытого подпроекта). Если не указан — используется ctx.worktree (корень git-репозитория)
directories string[] Условно Список путей к внешним директориям (относительных от root или абсолютных). Обязателен, если не указан compile_commands_dir
excludePatterns string[] Нет Glob-паттерны для исключения файлов (по умолчанию: ["node_modules", ".git", "dist"])
maxResults number Нет Верхний предел результатов из внешних директорий на один вызов (по умолчанию: 50). Фактический лимит динамически уменьшается, если основной поиск занял большую часть общего бюджета (см. ниже)
strict_path_restrictions boolean Нет При true перехватывает вызовы glob/grep и перенаправляет пути поиска за пределы configDir и внешних директорий обратно в configDir (по умолчанию: false). Подробнее см. Ограничение путей поиска
compile_commands_dir string Нет Относительный путь к директории с compile_commands.json относительно configDir. Плагин автоматически извлечёт уникальные директории исходных файлов из базы компиляции и будет использовать их как внешние директории для поиска. Если configDir не найден (opencode.json отсутствует) — парсинг compile_commands.json молча пропускается. Подробнее см. Поддержка compile_commands.json

Поле root

Плагин разрешает пути из directories относительно базовой директории. По умолчанию это ctx.worktree — корень git-репозитория, автоматически определяемый OpenCode. Если монорепа не использует git (или использует собственный VCS без маркера .git), OpenCode не сможет определить корень монорепы. В этом случае укажите root — путь от открытого подпроекта до корня монорепы:

{
  "root": "../../",
  "directories": ["shared-types", "common-utils"]
}

(Полный список параметров см. в таблице «Параметры конфигурации» и в примере ниже.)

Если root указан — пути из directories разрешаются от path.resolve(configDir || openDir, root), иначе — от ctx.worktree.

Форматы путей в directories

  • Относительные — разрешаются от базовой директории (определяемой root или ctx.worktree): "shared-types"
  • Абсолютные — используются как есть: "/opt/shared-libs"
  • Домашняя директория — поддерживается ~/: "~/projects/shared"

Как это работает

Перехват grep

Когда ИИ вызывает grep, плагин после выполнения встроенного поиска:

  1. Запускает ripgrep по всем настроенным внешним директориям (используя rg из PATH или из директории OpenCode). Параметр maxResults передаётся как --max-count, ограничивая количество совпадений на файл
  2. Если встроенный поиск нашёл результаты — дополняет их секцией --- External dependencies ---
  3. Если встроенный поиск не нашёл результатов — заменяет результат на внешние
  4. Обновляет счётчик совпадений в метаданных
  5. Строки длиннее 2000 символов обрезаются

Пример вывода с внешними результатами:

Found 3 matches

/home/user/monorepo/packages/my-app/src/main.ts:
  Line 10: import { UserProfile } from "../shared-types/types"

--- External dependencies ---
Found 2 matches

/home/user/monorepo/packages/shared-types/types.ts:
  Line 1: export interface UserProfile {
  Line 5: export type UserId = string;

Перехват glob

Аналогично grep — результаты поиска файлов во внешних директориях добавляются к результатам встроенного glob. Поиск файлов выполняется через нативный Bun.Glob. Если Bun.Glob недоступен — используется fallback через рекурсивный обход (readdirSync), который возвращает все файлы без применения glob-паттерна.

Инструмент deps_read

Кастомный инструмент для чтения файлов из внешних директорий:

  • Преимущество: не требует подтверждения external_directory для каждого файла
  • Параметры:
    • filePath (обязательный) — абсолютный путь к файлу
    • offset (необязательный) — начальная строка (с 1)
    • limit (необязательный) — максимальное количество строк (по умолчанию 2000)
  • Проверяет, что файл находится внутри настроенных внешних директорий
  • Файлы размером более 10 МБ отклоняются с предложением использовать offset/limit
  • Строки длиннее 2000 символов обрезаются
  • При запросе строк за пределами файла возвращается ошибка с указанием запрошенного диапазона и реального числа строк

Защита от дублирования и фильтрация путей

Плагин использует три механизма, чтобы избежать дублирования и пропускает внешний поиск, если он не нужен:

  1. Прямое совпадение с внешней директорией — если path указывает на одну из внешних директорий, внешний поиск пропускается (встроенный инструмент уже найдёт файлы там).
  2. Узкие пути поиска — если path является подкаталогом, не лежащим на прямом пути от openDir к configDir, внешний поиск не запускается (поиск по узкому пути не покрывает внешние директории).
  3. Исключение покрытых директорий — если path покрывает одну или несколько внешних директорий (например, path = корень монорепы), эти директории исключаются из внешнего поиска, но поиск по оставшимся директориям выполняется.

Ограничение путей поиска (strict_path_restrictions)

При включении опции strict_path_restrictions: true плагин перехватывает вызовы glob и grep до их выполнения через хук tool.execute.before. Если путь поиска (path) выходит за пределы configDir и внешних директорий — он перенаправляется в configDir. Это предотвращает поиск по произвольным областям файловой системы.

Ключевые особенности:

  • Хук регистрируется только при strict_path_restrictions === true и configDir !== null
  • Относительные пути разрешаются относительно openDir
  • Разрешённые области: внутри configDir или внутри любой из внешних директорий
  • Остальные параметры вызова (pattern, include и т.д.) не изменяются

Подробнее см. Ограничение путей поиска.

Ограничение общего бюджета результатов

Встроенные инструменты grep и glob в OpenCode возвращают не более 100 результатов каждый. Чтобы не нарушать это ограничение, плагин динамически рассчитывает бюджет для внешних результатов:

  1. После выполнения встроенного поиска плагин подсчитывает количество непустых строк в основном выводе
  2. Бюджет = max(0, 100 - количество_непустых_строк_основного_вывода)
  3. Фактический лимит внешних результатов = min(бюджет, maxResults) — параметр maxResults из конфига служит верхним пределом

Если бюджет = 0 (основной поиск вернул 100+ строк): внешний поиск не запускается. Вместо этого к выводу добавляется подсказка с абсолютными путями внешних директорий:

(External dependencies may contain additional matches: /home/user/my-monorepo/shared-types, /home/user/my-monorepo/common-utils.
Use the deps-read tool or search with grep/glob specifying an external directory path.)

Если бюджет > 0, но внешних результатов больше бюджета: поиск выполняется по всем внешним директориям (даже если первые уже заполнили лимит). Выводятся результаты, умещающиеся в бюджет. Затем добавляется подсказка, но только с теми директориями, где найдены результаты, но не все из них уместились в бюджет. Директории без результатов и те, чьи результаты полностью вошли в вывод, из подсказки исключаются.

Пример: основной grep вернул 90 результатов → бюджет = 10 → плагин добавит до 10 внешних совпадений. Если maxResults в конфиге = 5 — будет добавлено не более 5.

Пример: полная конфигурация монорепозитория

Структура проекта:

my-monorepo/
├── shared-types/          ← общие типы (корень монорепы)
├── common-utils/          ← общие утилиты (корень монорепы)
└── team-alpha/            ← директория команды
    ├── opencode.json
    ├── .opencode/
    │   └── plugins/
    │       └── ext-search/
    │           ├── package.json
    │           └── index.js ← из plugins/ext-search/dist
    └── my-app/            ← подпроект, открытый в OpenCode
        └── src/

team-alpha/opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": [
    [
      "./.opencode/plugins/ext-search",
      {
        "root": "../../",
        "directories": [
          "shared-types",
          "common-utils"
        ],
        "excludePatterns": ["node_modules", ".git", "dist"],
        "maxResults": 50,
        "strict_path_restrictions": false,
        "compile_commands_dir": null
      }
    ]
  ]
}

Глобальный конфиг (~/.config/opencode/opencode.json на Linux):

{
  "$schema": "https://opencode.ai/config.json",
  "permission": {
    "external_directory": {
      "/home/user/my-monorepo/shared-types/*": "allow",
      "/home/user/my-monorepo/common-utils/*": "allow"
    }
  }
}

Теперь при открытии my-monorepo/team-alpha/my-app в OpenCode:

  1. OpenCode найдёт my-monorepo/team-alpha/opencode.json
  2. Загрузит плагин из my-monorepo/team-alpha/.opencode/plugins/ext-search/
  3. Плагин определит корень монорепы через root (../../ от my-app → корень монорепы)
  4. Каждый вызов grep/glob будет автоматически дополняться результатами из shared-types и common-utils
  5. ИИ сможет использовать deps_read для чтения файлов из этих пакетов

Документация

Тестирование

Проект содержит три уровня тестов:

npm run test:unit        # юнит-тесты
npm run test:integration # интеграционные тесты
npm run test:e2e         # e2e-тесты
npm run test:all         # все уровни

Требования для e2e-тестов:

  • OpenCode установлен и доступен по пути ~/.opencode/bin/opencode (или задан через OPENCODE_BIN)
  • ripgrep доступен через PATH или в стандартных директориях OpenCode (для grep-тестов)

Тестовая фикстура эмулирует монорепу без git (нет .git), чтобы верифицировать работу поля root.

Совместимость

Плагин работает на всех основных операционных системах: Linux, macOS, Windows.

  • Glob-поиск использует нативный Bun.Glob (при недоступности — fallback через рекурсивный обход readdirSync)
  • Grep-поиск автоматически находит ripgrep (который OpenCode скачивает при установке) по стандартным путям для каждой платформы
  • Для выполнения команд используется Bun.spawn с fallback на child_process.execFileSync

Все пути обрабатываются через path.resolve() с учётом разделителей конкретной платформы.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors