---
pr: 77
title: Wire memory CLI into opencode-memory plugin via setup-memory.sh wrapper
---

## Что сделано
- `.opencode/scripts/setup-memory.sh`: добавлена `WRAPPER_PATH` env-переменная (default `/usr/local/lib/node_modules/@mathew-cf/opencode-memory/node_modules/@mathew-cf/rag-cli/bin/rag.js`, override через `MEMORY_WRAPPER_PATH`).
- `.opencode/scripts/setup-memory.sh`: добавлен выбор Python-интерпретатора `MEMORY_PYTHON` — prefer `${OPENCODE_WORKSPACE}/.venv/bin/python`, then `.venv/bin/python`, then `python3` (override через `MEMORY_WRAPPER_PYTHON`). Причина: system `python3` не имеет `httpx` (нет venv), CLI падает на import — wrapper должен вызывать venv-python.
- `.opencode/scripts/setup-memory.sh` шаг 5: `command -v rag` → `"$MEMORY_PYTHON" -c "import src.memory"`. Если доступен → `python -m src.memory index "$MEMORY_DIR" -o "$MEMORY_DIR/.rag"`. Если нет → echo skip + continue (best-effort, не exit 1).
- `.opencode/scripts/setup-memory.sh` шаг 5b (новый): генерация JS wrapper для плагина `@mathew-cf/opencode-memory`. Wrapper содержит `spawnSync(py, ["-m", "src.memory", ...process.argv.slice(2)], { cwd: OPENCODE_WORKSPACE })` — делегирует `rag index/search/download` в Python memory CLI. Idempotent через `cmp -s` (сравнение контента, НЕ `$(cat)` — bash `$()` удаляет trailing newline, давал false-negative). Backup `.orig` оригинала один раз (не перезаписывается). `chmod +x`.
- `tests/test_setup_memory.py`: `_run_script` расширен `wrapper: Path | None` параметром → `MEMORY_WRAPPER_PATH` env override. Все существующие тесты обновлены передавать `wrapper=tmp_path/"wrapper"/"rag.js"` (герметичность — не пишут в `/usr/local/lib/...`). insteadOf-тесты (прямые `subprocess.run`) тоже обновлены. mypy type-args починены (`CompletedProcess[str]`, `dict[str, str | None]`).
- `tests/test_setup_memory.py`: +4 новых теста — `test_wrapper_generated` (wrapper существует + executable), `test_wrapper_idempotent` (2 запуска → контент идентичен + "wrapper correct"), `test_wrapper_content` (содержит `python`, `-m`, `src.memory`, `spawnSync`, `process.argv.slice`), `test_wrapper_backup_original` (`.orig` сохраняет старый контент, НЕ перезаписывается при 2-й замене).
- `docs/project-map/README.md`: setup-memory.sh entry обновлён (6-step → 7-step, +JS wrapper), test_setup_memory.py entry обновлён (+wrapper gen/backup).
- ADR-033 + этот handoff.

## Почему
PR #75 (merged) отрефакторил Memory CLI (`second-brain` → `memory`, OpenAI env, chunking, batching, dedup), но плагин `@mathew-cf/opencode-memory` всё ещё вызывал родной Rust `@mathew-cf/rag-cli` — 5-10 мин на 1.2 MB на CPU, 3 зомби-процесса зафиксировано, индекс никогда не записан (`$OPENCODE_MEMORY_DIR/.rag` отсутствовал). Плагин находит rag через `require.resolve("@mathew-cf/rag-cli/bin/rag.js")` (вендорный `dist/index.js:12703-12708`, НЕ патчим).

Решение: setup-memory.sh генерирует JS wrapper по пути, который резолвит плагин. Wrapper = прагматичный shim (аналог pyenv/nvm shims), перехватывает `require.resolve` → перенаправляет вызовы в Python `memory` CLI. Идемпотентен (content check через `cmp -s`), backup `.orig` оригинала один раз. PR #2 в серии из 2 (после PR #75).

Отклонение от спеки: спека говорила `spawnSync("python3", ["-m", "src.memory", ...])` — но system `python3` не имеет `httpx` (нет venv), CLI падает на import. Реализация выбирает venv-python (`${OPENCODE_WORKSPACE}/.venv/bin/python` → `.venv/bin/python` → `python3`), wrapper встраивает выбранный путь. Зафиксировано в ADR-033 Альтернативы.

## Pending
—

## Watch out
- **`$(cat)` vs `cmp -s` для idempotency.** Bash `$()` удаляет trailing newlines — `$(cat file)` возвращает контент без `\n` в конце, а `$VAR` (одиночные кавычки) содержит `\n` перед закрывающей `'`. Сравнение `[ "$(cat)" = "$VAR" ]` = false даже при идентичном контенте. Решение: `cmp -s "$WRAPPER_PATH" /dev/stdin <<<"$WRAPPER_CONTENT"` — байт-точное сравнение. НЕ возвращаться к `$(cat)`.
- **Wrapper встраивает абсолютный путь к Python.** `MEMORY_PYTHON` разрешается на момент запуска setup-memory.sh и встраивается в wrapper как literal string. Если venv переедет — wrapper нужно перегенерировать (удалить wrapper, запустить setup-memory.sh). Альтернатива (читать env в runtime через `process.env.MEMORY_WRAPPER_PYTHON`) — оставлена как override, но default встраивается для надёжности (env может быть не set в контексте плагина).
- **`MEMORY_WRAPPER_PATH` env override обязателен для тестов.** Без него скрипт пишет в `/usr/local/lib/node_modules/@mathew-cf/opencode-memory/...` — real plugin path. Все тесты передают `wrapper=tmp_path/"wrapper"/"rag.js"`. Smoke-test может использовать real path (если плагин установлен).
- **Smoke-test пройден (2026-07-26):** `bash .opencode/scripts/setup-memory.sh` с `MEMORY_WRAPPER_PATH=/tmp/...` → wrapper сгенерирован, `.rag/index.json` создан через облако (1 файл, ~42KB), idempotency подтверждена (2-й запуск → "wrapper correct"), backup `.orig` работает. Реальный plugin path НЕ тестировался (плагин может быть не установлен в тестовом окружении).