opencode-config/.opencode/tools/memory-doctor.ts
Sergey 4cf149cb02
docs(memory): reindex + update SKILL.md (#104)
* feat(memory): add OPENAI_EMBEDDING_BATCH_DELAY support

* docs(memory): update SKILL.md for progressive enhancement and auto-setup

* docs(memory): update AGENTS.md tool usage policy

* docs(memory): fix stale snake_case tool references in skills, agents, tools

* docs(handoff): add handoff + ADR for reindex-skill-update

* docs(handoff): set PR number

* refactor(memory): extract _embed_in_batches to satisfy xenon rank A

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-27 02:08:35 +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")
},
})