---
pr: 101
title: feat(memory): 5 TS tools with auto-setup and fallback
---

## Что сделано
5 TS tools в `.opencode/tools/` заменяющие плагин `@mathew-cf/opencode-memory`. Прогрессивная модель: keyword (ripgrep) работает всегда, semantic (Python+OpenRouter) как enhancement с fallback. Zero-config: первый `memory-save` авто-инициализирует память.

- `memory-save.ts` (~120 LOC) — auto-setup + git commit + async reindex. Определяет `memoryDir` (env или `~/.local/share/opencode/opencode-memory`). При первом вызове: `mkdir -p`, `git init` (или `git clone` если `OPENCODE_MEMORY_REMOTE` set и dir пуста), установка post-commit hook (`git push origin master 2>/dev/null || true`) при наличии remote, создание 7 категорий. Затем `git add -A` + `git diff --cached --name-only` → если пусто, return "No changes to sync". `git commit -m "memory: sync <files>"`. Async reindex: `spawn("python3", ["-m","src.memory","index",memoryDir,"-o",.rag], {cwd: context.worktree, stdio:"ignore", detached:true}) + child.unref()` — fire-and-forget, НЕ блокирует ответ. Лог в `.rag/reindex.log` (Python из PR#100). Skip semantic если `OPENAI_BASE_URL` не set.
- `memory-search.ts` (~270 LOC) — ripgrep + Python semantic + scoring + fallback. `resolveRgBinary()`: `require2("@vscode/ripgrep").rgPath` (если установлен и существует), fallback на системный `rg`. Keyword: `spawnSync(rgBin, [...buildRgArgs(terms), searchDir])` — `--glob *.md --glob !.git --glob !.rag --glob !**/INDEX.md -e <term>` per term (OR logic). Semantic: если `OPENAI_BASE_URL` set AND `.rag/index.json` существует → `spawnSync("python3", ["-m","src.memory","search",query,"-i",.rag,"-k","15","--json"], {cwd: context.worktree})`. Если Python упал (status≠0) / вернул `[]` / env не set → skip semantic (fallback keyword-only). Scoring — порт `scoreCandidate()` из плагина `dist/index.js:12822-12852`: rgMatch (+0.15+0.35*termCoverage), ragScore×1.4, rgMatch+ragScore bonus +0.10, tags overlap +0.20*(tagHits/terms), path hits +0.15, importance (high +0.15, low -0.10), accessCount (≥5 +0.10, ≥2 +0.05). Cross-category fallback если category filter дал 0 hits. Output: markdown с top-7 результатами + Related files.
- `memory-list.ts` (~100 LOC) — pure TS, без subprocess. Без `category` → список 7 категорий с count .md файлов (`walkMd` рекурсивно, skip `.git`/`.rag`/`INDEX.md`). С `category` → список файлов (`{path, title, summary, importance, updated}` из frontmatter), отсортированы по `updated` descending.
- `memory-access.ts` (~70 LOC) — pure TS, bump frontmatter. Читает файл, парсит frontmatter, обновляет `last_accessed: <today YYYY-MM-DD>` и `access_count: <current+1>` (regex replace, порт `bumpAccessFields` из плагина `dist/index.js:12677-12694`). Atomic write: `writeFileSync(tmp) + renameSync(tmp,filePath)`. НЕ коммитит — следующий `memory-save` синхронизирует.
- `memory-doctor.ts` (~85 LOC) — read-only диагностика. Проверяет: `rg` (через `@vscode/ripgrep` + fallback system rg + `spawnSync --version`), Python `src.memory` importable (`python3 -c "import src.memory"`, cwd=worktree), env vars (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENCODE_MEMORY_REMOTE`), `OPENCODE_MEMORY_DIR` exists, memory dir is git repo, `.rag/index.json` exists (size+mtime), count .md files. Возвращает markdown с ✅/❌ по каждому пункту + summary (all-green / keyword-only / rg missing).
- `.opencode/package.json` — добавлены deps: `@vscode/ripgrep ^1.18.0` (keyword search, платформенный binary `@vscode/ripgrep-linux-x64/bin/rg`), devDeps: `@types/node ^26.1.1` (typecheck) + `typescript ^7.0.2` (typecheck).
- ADR-043 + этот handoff.

Scoring logic портирован 1-в-1 из плагина (rgMatch 0.15+0.35*termCoverage, ragScore×1.4, +0.10 за совпадение обоих, tags +0.20, path +0.15, importance, accessCount). Тестирование: typecheck (`tsc --noEmit --target es2022 --moduleResolution bundler --types node`) — clean. Smoke-tests всех 5 tools на реальной памяти (84 .md файла, 210 MB index.json): memory-search (6 keyword matches по "embedder"), memory-list (preferences 0 / repos 19 / technical 63 / workflows 2), memory-access (3→4 count, atomic), memory-doctor (all green), memory-save (auto-setup на пустой /tmp dir с git init + hook + remote + commit).

## Почему
Плагин `@mathew-cf/opencode-memory` не вызывается из-за бага wrapper-пути MCP server (`MEMORY_WRAPPER_PATH` указывает на `@mathew-cf/rag-cli/bin/rag.js`, который не существует/несовместим — см. ADR-030, PR#57, PR#71). TS tools вызывают Python через `spawnSync` (паттерн `pipeline-status.ts`, `commit.ts`, `memory-setup.ts`) — нативный, без wrapper-hop. Это Фаза 2 миграции к единой облачной памяти (issue #94, ADR-030): Фаза 1 (PR#100) сделала embedder lazy-init (returns None без env), Фаза 2 (этот PR) — TS tools, Фаза 3 — удалить плагин + `memory-setup.ts` + `setup-memory.sh`.

Прогрессивная модель (progressive enhancement): keyword (ripgrep) работает ВСЕГДА, semantic (Python+OpenRouter) как enhancement с fallback на keyword-only. Zero-config: первый `memory-save` авто-инициализирует память (mkdir + git init/clone + hook + 7 категорий) — не требует отдельного `memory-setup` вызова. Это упрощает onboarding и убирает зависимость от `OPENCODE_MEMORY_REMOTE` (хотя hook auto-push ставится только если remote set).

## Pending
—
- Фаза 3 (отдельный PR): удалить `.opencode/tools/memory-setup.ts` + `.opencode/scripts/setup-memory.sh` + плагин `@mathew-cf/opencode-memory` из `opencode.json` `plugin` block + обновить AGENTS.md Tool Usage Policy таблицу (10 tools → 14 tools: добавить `memory-save`, `memory-search`, `memory-list`, `memory-access`, `memory-doctor`; убрать `memory-setup`).

## Watch out
- **`@vscode/ripgrep` платформенный binary** — `require("@vscode/ripgrep").rgPath` возвращает путь к `@vscode/ripgrep-<platform>/bin/rg` (не к `@vscode/ripgrep/bin/rg`). На linux-x64: `@vscode/ripgrep-linux-x64/bin/rg`. Проверяется через `fs.existsSync(mod.rgPath)`. Если платформенный пакет не установлен (unsupported platform) → fallback на системный `rg` через `spawnSync("rg", ["--version"])`. Если оба недоступны → memory-search возвращает "No memories found" (keyword не работает, semantic может). memory-doctor показывает ❌.
- **`OPENCODE_MEMORY_REMOTE` опционален для memory-save** (в отличие от `setup-memory.sh`, который exit 1 без remote). memory-save авто-инициализирует git repo (`git init`) даже без remote — hook auto-push не ставится. Это делает zero-config возможным (новый пользователь без remote может начать писать память, push настроит позже через `OPENCODE_MEMORY_REMOTE`). Hook ставится ТОЛЬКО если remote set AND hook отсутствует/несовпадает.
- **`spawn(... detached: true) + child.unref()`** для async reindex — fire-and-forget, НЕ блокирует return memory-save. Если процесс opencode умрёт до завершения reindex, orphaned python процесс может остаться (но `.rag/.lock` через `fcntl.flock(LOCK_EX)` в `index.py` предотвращает concurrent reindex corruption). Reindex логирует прогресс в `.rag/reindex.log` (из PR#100).
- **Scoring weights портированы 1-в-1** из плагина `scoreCandidate` (`dist/index.js:12822-12852`). НЕ изменяй веса без ADR — это нарушит совместимость результатов поиска между плагином (если останется) и TS tools. Path hits +0.15 — это было в плагине (`input.path.toLowerCase().replace(/[-_/.]/g, " ")`), сохранено.
- **Typecheck требует `@types/node` + `typescript`** — добавлены как devDeps в `.opencode/package.json`. CI runner должен делать `npm install` в `.opencode/` перед typecheck. Runtime (opencode plugin loader) использует Bun/Node native TS, эти devDeps не нужны в production.
- **Frontmatter parsing упрощён** (regex `^(\w+):\s*(.+)$` per line, не полноценный YAML parser) — покрывает наш формат (плоские ключи + `[a, b]` для tags/related). Многострочные значения НЕ поддерживаются. Это тот же парсер что в плагине (`parseFrontmatter` `dist/index.js:12639-12676`).
- **memory-search cross-category fallback** — если `args.category` указан и дал 0 hits, search повторяет по всему `memoryDir` (без category filter). Это поведение плагина (`crossCategoryFallback`, `dist/index.js:12999-13022`). Сохранено для UX.
- **`spawnSync("python3", ["-c", "import src.memory"], {cwd: context.worktree})`** в memory-doctor — требует что `context.worktree` имеет `src/memory/` пакет importable (`.venv/bin/python` или system python с `src.memory` установлен). Если worktree не настроен (нет `.venv`, нет `pip install -e .`) → ❌, но это диагностический сигнал, не блокер.
- **`git -C memoryDir commit`** через `spawnSync` — НЕ через `commit` tool (тот для main repo, с conventional format validation). Memory repo commit message: `memory: sync <files>` (derived, не conventional format). Это тот же паттерн что в плагине `runSave` (`dist/index.js:13259-13260`).