From 89a4d947fdef4b23ac7a15423c56a7a0043570a2 Mon Sep 17 00:00:00 2001 From: Sergey <93754860+slaid098@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:07:18 +0300 Subject: [PATCH] fix(docker): install opencode-memory plugin in Dockerfile (#57) * fix(docker): install @mathew-cf/opencode-memory plugin in Dockerfile * test(docker): add Dockerfile npm install test * docs(handoff): set PR number 57 in handoff + ADR-024 --------- Co-authored-by: opencode-agent --- Dockerfile | 2 +- .../024-pr-57-memory-plugin-dockerfile.md | 36 ++++++ .../handoff/pr-57-memory-plugin-dockerfile.md | 45 +++++++ docs/project-map/README.md | 3 +- tests/test_dockerfile.py | 118 ++++++++++++++++++ 5 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 docs/decisions/024-pr-57-memory-plugin-dockerfile.md create mode 100644 docs/handoff/pr-57-memory-plugin-dockerfile.md create mode 100644 tests/test_dockerfile.py diff --git a/Dockerfile b/Dockerfile index 3ad5106..d1de0ec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/c ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true -RUN npm install -g opencode-ai repomix --unsafe-perm +RUN npm install -g opencode-ai repomix @mathew-cf/opencode-memory --unsafe-perm WORKDIR /root/workspace diff --git a/docs/decisions/024-pr-57-memory-plugin-dockerfile.md b/docs/decisions/024-pr-57-memory-plugin-dockerfile.md new file mode 100644 index 0000000..49df014 --- /dev/null +++ b/docs/decisions/024-pr-57-memory-plugin-dockerfile.md @@ -0,0 +1,36 @@ +# ADR-024 (PR #57): Install @mathew-cf/opencode-memory plugin in Dockerfile + +## Статус +Accepted (2026-07-24) + +## Контекст + +`opencode.json` ссылается на MCP plugin `@mathew-cf/opencode-memory` с конфигурацией `memoryDir`. Плагин предоставляет три MCP tool'а: `memory_save` (коммит в memory repo, используется memory-syncer subagent'ом), `memory_search` (semantic + keyword search по заметкам), `memory_list` (browsing по категориям). + +Однако `Dockerfile` не устанавливал плагин через `npm install -g` — строка содержала только `opencode-ai` и `repomix`. Без установки opencode молча пропускает плагин, на который ссылается конфиг: MCP tools недоступны, memory-syncer не может коммитить, search/list не работают. + +Дополнительно: `@mathew-cf/opencode-memory` тянет `@mathew-cf/rag-cli` как dependency для semantic search. RAG CLI — native binary, требует **GLIBC 2.39**. Базовый image `node:20-slim` (Debian Bookworm) поставляется с **GLIBC 2.36**. Semantic search через RAG не запускается (несовместимость glibc). Memory plugin деградирует до keyword search (grep) — этого достаточно для базовых сценариев `memory_search` и `memory_list`. + +## Решение + +Добавить `@mathew-cf/opencode-memory` в существующую строку `npm install -g` в `Dockerfile`: + +```dockerfile +RUN npm install -g opencode-ai repomix @mathew-cf/opencode-memory --unsafe-perm +``` + +Один RUN layer, все три пакета ставятся вместе. Дополнительно — 6 тестов в `tests/test_dockerfile.py` (parsed + raw check на наличие плагина и `opencode-ai`, anti-split check на одну строку install), чтобы предотвратить регрессию. + +### Semantic search / RAG CLI + +GLIBC-несовместимость RAG CLI на `node:20-slim` — **известная проблема, НЕ блокер**. Memory plugin работает с keyword search (grep) без RAG. Полный fix semantic search (смена base image на `node:20-bookworm` с обновлённым glibc, или `node:20` на Debian Trixie с GLIBC 2.40) — вне scope этого PR. + +## Альтернативы + +- **Отдельный RUN layer для `@mathew-cf/opencode-memory`** — отклонено: лишний image layer, дублирование `--unsafe-perm`, затрудняет grep-проверку (тест `test_single_npm_install_line_has_all_packages` явно запрещает разбивку). Все пакеты ставятся в один layer. + +- **Установить плагин через opencode plugin auto-load (копирование в `~/.local/share/opencode/plugins/`)** — отклонено: `opencode.json` уже ссылается на пакет по имени (`"@mathew-cf/opencode-memory"`), npm install — стандартный путь установки npm-пакетов. Копирование файлов дублировало бы установку и сломало бы update path (`npm update -g`). + +- **Сменить base image на `node:20-bookworm` (GLIBC 2.36 → 2.40) для RAG CLI** — отклонено: `node:20-bookworm` (full image) весит ~1GB против ~200MB `node:20-slim`, тянет лишние пакеты. Semantic search — опциональная фича, keyword search покрывает базовые сценарии. Смена base image — отдельная задача с тестированием всех зависимостей (chromium, gh, cloudflared, ffmpeg). + +- **Не устанавливать плагин, убрать ссылку из `opencode.json`** — отклонено: memory-syncer subagent и memory workflow зависят от MCP tools. Без плагина memory pipeline (MEMORY phase в pipeline-driver) не работает. \ No newline at end of file diff --git a/docs/handoff/pr-57-memory-plugin-dockerfile.md b/docs/handoff/pr-57-memory-plugin-dockerfile.md new file mode 100644 index 0000000..583fb8d --- /dev/null +++ b/docs/handoff/pr-57-memory-plugin-dockerfile.md @@ -0,0 +1,45 @@ +--- +pr: 57 +title: install @mathew-cf/opencode-memory plugin in Dockerfile +--- + +# PR #57: install @mathew-cf/opencode-memory plugin in Dockerfile + +## Что сделано +- `Dockerfile:34` — в строку `npm install -g opencode-ai repomix --unsafe-perm` добавлен `@mathew-cf/opencode-memory`. Итоговая строка: + ```dockerfile + RUN npm install -g opencode-ai repomix @mathew-cf/opencode-memory --unsafe-perm + ``` + Единственное изменение в Dockerfile, больше ничего не трогалось. +- `tests/test_dockerfile.py` — новый файл, 6 тестов: + - `test_dockerfile_exists` — файл существует на repo root + - `test_memory_plugin_in_npm_install` — `npm install -g` строка содержит `@mathew-cf/opencode-memory` (parsed check по строкам с `npm install -g`) + - `test_memory_plugin_in_npm_install_raw` — raw text содержит `@mathew-cf/opencode-memory` (belt-and-suspenders) + - `test_opencode_ai_present` — `npm install -g` строка содержит `opencode-ai` (regression guard — не удалить случайно) + - `test_opencode_ai_present_raw` — raw text содержит `opencode-ai` + - `test_single_npm_install_line_has_all_packages` — все три пакета (`opencode-ai`, `repomix`, `@mathew-cf/opencode-memory`) в одной строке install (anti-split: запрещает разбивать install по нескольким RUN layer'ам) + - Проверки raw-text grep (без Docker build), по паттерну `test_docker_compose.py` (parsed + raw, belt-and-suspenders). +- ADR-024 + этот handoff. + +## Почему +После миграции opencode-config на linux-1 обнаружено: `@mathew-cf/opencode-memory` MCP plugin НЕ установлен в контейнере. `opencode.json` ссылается на плагин: +```json +"@mathew-cf/opencode-memory", +{ "memoryDir": "{env:OPENCODE_MEMORY_DIR}" } +``` +Но без `npm install -g` opencode молча пропускает плагин — MCP tools недоступны: +- `memory_save` — memory-syncer subagent не может коммитить в memory repo +- `memory_search` — нет semantic/keyword search +- `memory_list` — нет browsing по категориям + +Добавление пакета в существующую строку `npm install -g` — минимальное изменение: один layer, все три пакета ставятся за один RUN, без нового layer'а. + +## Pending +— (нет) + +## Watch out +- **RAG CLI требует GLIBC 2.39, node:20-slim имеет GLIBC 2.36 — semantic search НЕ работает.** `@mathew-cf/opencode-memory` тянет `@mathew-cf/rag-cli` как dependency для semantic search. RAG CLI (native binary) требует GLIBC 2.39, а `node:20-slim` (Debian Bookworm) поставляется с GLIBC 2.36. Semantic search через RAG падает с ошибкой совместимости glibc. Memory plugin работает с **keyword search (grep)** без RAG — этого достаточно для `memory_search` и `memory_list`. Semantic search — опциональная фича, это **известная проблема, НЕ блокер**. Workaround: keyword search покрывает базовые сценарии. Полный fix semantic search потребует другого base image (например, `node:20-bookworm` с обновлённым glibc, или `node:20` на Debian Trixie с GLIBC 2.40) — вне scope этого PR. +- **Все три пакета в одной строке install** — тест `test_single_npm_install_line_has_all_packages` намеренно запрещает разбивку install по нескольким RUN layer'ам. Разбиение добавило бы лишние image layers и скрыло бы, какой пакет отсутствует, при grep-проверке Dockerfile. +- **Anti-regression тесты (raw + parsed)** — дублирующие raw-проверки добавлены намеренно (как в `test_docker_compose.py`): parsed-check может пропустить edge case (многострочный install, кавычки), raw-check ловит строку напрямую. Оба должны оставаться. +- **`opencode-ai` regression guard** — тест `test_opencode_ai_present` (+ raw) защищает от случайного удаления `opencode-ai` при редактировании строки install. `opencode-ai` — основной пакет, без него контейнер не запустится (ENTRYPOINT `opencode`). +- ADR number = 024 (sequential, следующий после 023), НЕ PR number. \ No newline at end of file diff --git a/docs/project-map/README.md b/docs/project-map/README.md index 74e4e81..208c524 100644 --- a/docs/project-map/README.md +++ b/docs/project-map/README.md @@ -79,6 +79,7 @@ opencode-config/ │ ├── test_check_permissions.py # permissions-check.yml validator │ ├── test_cli.py # src/memory/cli.py │ ├── test_docker_compose.py # docker-compose.yml port exposure (0.0.0.0:4096, no 127.0.0.1) — PR#51 +│ ├── test_dockerfile.py # Dockerfile npm install (opencode-ai + repomix + @mathew-cf/opencode-memory) — PR#57 │ ├── test_commit_tool.py # .opencode/tools/commit.ts (via _ts_loader.mjs exec_stub_json) — PR#38 │ ├── test_commit_tool.ts # TS wrapper test (mjs loader) — PR#38 │ ├── test_create_issue_tool.py # .opencode/tools/create-issue.ts (via _ts_loader.mjs exec_stub_json) — PR#38 @@ -109,7 +110,7 @@ opencode-config/ ├── uv.lock # Locked deps for Python project ├── .pre-commit-config.yaml # ruff + UV hooks ├── docker-compose.yml # 2 services (dind + opencode), opencode_network, 4 bind mounts, port 4096 on 0.0.0.0 — PR#24, PR#51 -├── Dockerfile # node:20-slim + uv + gh + chromium + docker.io + opencode-ai + repomix + cloudflared — PR#24, PR#34 +├── Dockerfile # node:20-slim + uv + gh + chromium + docker.io + opencode-ai + repomix + @mathew-cf/opencode-memory + cloudflared — PR#24, PR#34, PR#57 ├── .env.example # Placeholder-only env template (user copies to .env) — PR#24, PR#34 (TUNNEL_DOMAIN), PR#36 (OPENCODE_MEMORY_REMOTE/DIR) ├── app_data/ │ ├── opencode-memory/ # Persistent memory (separate git repo, gitignored) — PR#36 diff --git a/tests/test_dockerfile.py b/tests/test_dockerfile.py new file mode 100644 index 0000000..cf41bc2 --- /dev/null +++ b/tests/test_dockerfile.py @@ -0,0 +1,118 @@ +"""Tests for Dockerfile npm install line. + +Covers issue #56 acceptance criteria: +- ``Dockerfile`` installs ``@mathew-cf/opencode-memory`` alongside + ``opencode-ai`` and ``repomix`` in the global npm install step, so the + memory plugin (``memory_save`` / ``memory_search`` / ``memory_list`` MCP + tools) is available inside the container. +- ``opencode-ai`` is still present (regression guard — must not be dropped + when editing the install line). + +Checks are raw-text grep assertions (no Docker build needed), mirroring the +belt-and-suspenders style of ``test_docker_compose.py``. +""" + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +DOCKERFILE = REPO_ROOT / "Dockerfile" + +REQUIRED_PACKAGES = ("opencode-ai", "repomix", "@mathew-cf/opencode-memory") +MEMORY_PLUGIN = "@mathew-cf/opencode-memory" +OPENCODE_AI = "opencode-ai" + + +def _dockerfile_text() -> str: + """Read Dockerfile text (asserts the file exists).""" + assert DOCKERFILE.exists(), f"Dockerfile missing: {DOCKERFILE}" + return DOCKERFILE.read_text() + + +def _npm_install_lines(text: str) -> list[str]: + """Return every ``npm install -g ...`` line from the Dockerfile.""" + return [line for line in text.splitlines() if "npm install -g" in line] + + +# ── file exists ───────────────────────────────────────────────────────────── + + +def test_dockerfile_exists(): + """Dockerfile exists at repo root.""" + assert DOCKERFILE.exists(), f"Dockerfile missing: {DOCKERFILE}" + + +# ── @mathew-cf/opencode-memory installed (issue #56 — the required plugin) ─── + + +def test_memory_plugin_in_npm_install(): + """Dockerfile installs ``@mathew-cf/opencode-memory`` via ``npm install -g``. + + Without the plugin the memory MCP tools (``memory_save`` / + ``memory_search`` / ``memory_list``) are unavailable — opencode silently + skips the uninstalled plugin referenced in opencode.json. + """ + lines = _npm_install_lines(_dockerfile_text()) + assert lines, "no `npm install -g` line found in Dockerfile" + assert any(MEMORY_PLUGIN in line for line in lines), ( + f"Dockerfile must install {MEMORY_PLUGIN!r} via `npm install -g` — " + "memory plugin MCP tools depend on it" + ) + + +def test_memory_plugin_in_npm_install_raw(): + """Raw text check: the Dockerfile contains ``@mathew-cf/opencode-memory``. + + Belt-and-suspenders alongside the parsed check — catches edge cases where + the line collector might miss a multi-line or differently-formatted + install statement. + """ + assert MEMORY_PLUGIN in _dockerfile_text(), ( + f"Dockerfile must contain {MEMORY_PLUGIN!r} so the memory plugin loads inside the container" + ) + + +# ── opencode-ai still present (regression — must not drop the main package) ── + + +def test_opencode_ai_present(): + """Dockerfile still installs ``opencode-ai`` (regression guard). + + Editing the npm install line to add the memory plugin must not + accidentally drop the primary ``opencode-ai`` package. + """ + lines = _npm_install_lines(_dockerfile_text()) + assert lines, "no `npm install -g` line found in Dockerfile" + assert any(OPENCODE_AI in line for line in lines), ( + f"Dockerfile must still install {OPENCODE_AI!r} — dropping it breaks " + "the container entrypoint" + ) + + +def test_opencode_ai_present_raw(): + """Raw text check: the Dockerfile contains ``opencode-ai``.""" + assert OPENCODE_AI in _dockerfile_text(), f"Dockerfile must still contain {OPENCODE_AI!r}" + + +# ── single npm install line holds all three packages ──────────────────────── + + +def test_single_npm_install_line_has_all_packages(): + """One ``npm install -g`` line installs all three required packages. + + Guards against splitting the install across multiple RUN layers (which + would waste image layers and obscure which package is missing). + """ + lines = _npm_install_lines(_dockerfile_text()) + install_lines = [line for line in lines if OPENCODE_AI in line] + assert install_lines, f"no `npm install -g` line with {OPENCODE_AI!r}" + main_line = install_lines[0] + for pkg in REQUIRED_PACKAGES: + assert pkg in main_line, ( + f"package {pkg!r} missing from the opencode-ai npm install line: {main_line!r}" + ) + + +if __name__ == "__main__": + import pytest + + pytest.main([__file__, "-v"])