import { spawnSync } from "child_process" import fs from "fs" import path from "path" import { tool } from "@opencode-ai/plugin" import { resolveMemoryDir, resolveRgBinary, walkMd } from "./_memory-shared" function mark(ok: boolean): string { return ok ? "✅" : "❌" } function checkRipgrep(rgBin: string | null): string[] { const lines: string[] = [] if (rgBin) { const r = spawnSync(rgBin, ["--version"], { encoding: "utf-8" }) const ver = (r.stdout || "").split("\n")[0].trim() lines.push(`- ${mark(r.status === 0)} ripgrep (keyword search): ${rgBin}${ver ? ` — ${ver}` : ""}`) } else { const sys = spawnSync("rg", ["--version"], { encoding: "utf-8" }) if (sys.status === 0) { lines.push(`- ${mark(true)} ripgrep (keyword search): system rg on $PATH`) } else { lines.push(`- ❌ ripgrep (keyword search): NOT resolvable — install @vscode/ripgrep or system rg`) } } return lines } function checkPythonMemory(worktree: string): { lines: string[]; ok: boolean } { const pyR = spawnSync("python3", ["-c", "import src.memory; print('ok')"], { encoding: "utf-8", cwd: worktree, }) const lines = [ `- ${mark(pyR.status === 0)} Python src.memory importable (semantic search): ${ pyR.status === 0 ? "yes" : `no — ${(pyR.stderr || pyR.stdout || "").trim().split("\n").pop() || ""}` }`, ] return { lines, ok: pyR.status === 0 } } function checkEnvVars(): string[] { return [ `- ${mark(!!process.env.OPENAI_BASE_URL)} OPENAI_BASE_URL set (embeddings endpoint): ${ process.env.OPENAI_BASE_URL ? "✓" : "not set — semantic search disabled, keyword still works" }`, `- ${mark(!!process.env.OPENAI_API_KEY)} OPENAI_API_KEY set (embeddings auth): ${ process.env.OPENAI_API_KEY ? "✓" : "not set" }`, `- ${mark(!!process.env.OPENCODE_MEMORY_REMOTE)} OPENCODE_MEMORY_REMOTE set (auto-push to git): ${ process.env.OPENCODE_MEMORY_REMOTE ? "✓" : "not set — auto-push disabled" }`, ] } function checkIndex(memoryDir: string): string[] { const lines: string[] = [] lines.push( `- ${mark(fs.existsSync(memoryDir))} OPENCODE_MEMORY_DIR exists: ${memoryDir}${ fs.existsSync(memoryDir) ? "" : "— run memory_save to auto-setup" }` ) if (!fs.existsSync(memoryDir)) return lines const gitDir = path.join(memoryDir, ".git") lines.push( `- ${mark(fs.existsSync(gitDir))} memory dir is a git repo: ${ fs.existsSync(gitDir) ? "yes" : "no — will be init on first memory_save" }` ) const indexJson = path.join(memoryDir, ".rag", "index.json") lines.push( `- ${mark(fs.existsSync(indexJson))} RAG index exists: ${ fs.existsSync(indexJson) ? path.relative(memoryDir, indexJson) : "not built — first memory_save with OPENAI_BASE_URL will trigger reindex" }` ) if (fs.existsSync(indexJson)) { try { const stat = fs.statSync(indexJson) const sizeMb = (stat.size / 1024 / 1024).toFixed(1) lines.push(` (size: ${sizeMb} MB, modified: ${stat.mtime.toISOString().slice(0, 10)})`) } catch (e) { process.stderr.write(`[memory-doctor] stat index.json failed: ${(e as Error).message}\n`) } } try { const mdCount = walkMd(memoryDir).length lines.push(`- ℹ️ memory files (.md): ${mdCount}`) } catch (e) { process.stderr.write(`[memory-doctor] walkMd failed: ${(e as Error).message}\n`) } return lines } function formatDoctorReport(rgBin: string | null, pyOk: boolean, memoryDir: string): string { const indexJson = path.join(memoryDir, ".rag", "index.json") const allGreen = rgBin !== null && pyOk && fs.existsSync(memoryDir) && fs.existsSync(indexJson) if (allGreen) return "All green — both keyword and semantic search available." if (rgBin !== null) return "Keyword search works. Semantic search unavailable — see ❌ items above." return "ripgrep missing — keyword search will return no results. Install @vscode/ripgrep." } export default tool({ description: "Read-only diagnostic for the memory subsystem. Reports: ripgrep (keyword) availability, Python src.memory importability, " + "env vars (OPENAI_BASE_URL, OPENAI_API_KEY, OPENCODE_MEMORY_REMOTE), memory dir existence, RAG index existence. " + "Does NOT modify anything. Run when memory_search/memory_save misbehave or to verify setup.", args: {}, async execute(_args, context) { const memoryDir = resolveMemoryDir() const lines: string[] = ["## Memory Doctor\n"] const rgBin = resolveRgBinary() lines.push(...checkRipgrep(rgBin)) const py = checkPythonMemory(context.worktree) lines.push(...py.lines) lines.push(...checkEnvVars()) lines.push(...checkIndex(memoryDir)) lines.push("") lines.push(formatDoctorReport(rgBin, py.ok, memoryDir)) return lines.join("\n") }, })