* 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>
128 lines
No EOL
4.8 KiB
TypeScript
128 lines
No EOL
4.8 KiB
TypeScript
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")
|
||
},
|
||
}) |