* feat(memory): memory-save tool with auto-setup * feat(memory): memory-search with keyword and semantic fallback * feat(memory): memory-list and memory-access tools * feat(memory): memory-doctor diagnostic tool * docs(handoff): add handoff and ADR for ts memory tools * docs(handoff): set PR number * docs(handoff): finalize pr-101 frontmatter and pending * docs(project-map): add 5 memory TS tools after PR#101 * refactor(memory): extract shared helpers to _memory-shared.ts * refactor(memory): split oversized execute functions * fix(memory): handle git checkout and catch blocks --------- Co-authored-by: opencode-agent <agent@opencode.local>
10 KiB
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_REMOTEset и 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_URLset 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.35termCoverage), 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), отсортированы поupdateddescending.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), Pythonsrc.memoryimportable (python3 -c "import src.memory", cwd=worktree), env vars (OPENAI_BASE_URL,OPENAI_API_KEY,OPENCODE_MEMORY_REMOTE),OPENCODE_MEMORY_DIRexists, memory dir is git repo,.rag/index.jsonexists (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.jsonpluginblock + обновить 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). Многострочные значения НЕ поддерживаются. Это тот же парсер что в плагине (parseFrontmatterdist/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— НЕ черезcommittool (тот для main repo, с conventional format validation). Memory repo commit message:memory: sync <files>(derived, не conventional format). Это тот же паттерн что в плагинеrunSave(dist/index.js:13259-13260).