opencode-config/.opencode/tools/memory-doctor.ts
Sergey 45bbdc50f3
feat(memory): 5 TS tools with auto-setup and fallback (#101)
* 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>
2026-07-27 01:12:51 +03:00

128 lines
No EOL
4.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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")
},
})