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 <agent@slaid098.dev>
This commit is contained in:
parent
e413a6c132
commit
89a4d947fd
5 changed files with 202 additions and 2 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
36
docs/decisions/024-pr-57-memory-plugin-dockerfile.md
Normal file
36
docs/decisions/024-pr-57-memory-plugin-dockerfile.md
Normal file
|
|
@ -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) не работает.
|
||||
45
docs/handoff/pr-57-memory-plugin-dockerfile.md
Normal file
45
docs/handoff/pr-57-memory-plugin-dockerfile.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
118
tests/test_dockerfile.py
Normal file
118
tests/test_dockerfile.py
Normal file
|
|
@ -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"])
|
||||
Loading…
Add table
Reference in a new issue