import { spawn, spawnSync } from "child_process" import fs from "fs" import path from "path" import { tool } from "@opencode-ai/plugin" import { CATEGORIES, resolveMemoryDir } from "./_memory-shared" const HOOK_BODY = "#!/bin/bash\ngit push origin master 2>/dev/null || true\n" function run(cmd: string, args: string[], opts?: { cwd?: string }) { return spawnSync(cmd, args, { encoding: "utf-8", ...opts }) } function isEmptyDir(dir: string): boolean { if (!fs.existsSync(dir)) return true return fs.readdirSync(dir).filter((n) => !n.startsWith(".")).length === 0 } function autoSetup(memoryDir: string, remote: string | undefined): string[] { const steps: string[] = [] if (!fs.existsSync(memoryDir)) { fs.mkdirSync(memoryDir, { recursive: true }) steps.push(`created directory: ${memoryDir}`) } if (!fs.existsSync(path.join(memoryDir, ".git"))) { if (remote && isEmptyDir(memoryDir)) { const r = run("git", ["clone", "--origin", "origin", remote, memoryDir]) if (r.status !== 0) { run("git", ["init", memoryDir]) steps.push(`git init (clone failed: ${(r.stderr || "").trim()})`) } else { const co = run("git", ["-C", memoryDir, "checkout", "master"]) if (co.status !== 0) { process.stderr.write( `[memory-save] git checkout master failed (exit ${co.status}): ${(co.stderr || co.stdout || "").trim()}\n` ) steps.push("cloned remote (checkout master failed — check stderr)") } else { steps.push(`cloned remote: ${remote}`) } } } else { run("git", ["init", memoryDir]) steps.push("git init") } } if (remote) { const hookPath = path.join(memoryDir, ".git", "hooks", "post-commit") let needHook = true if (fs.existsSync(hookPath)) { if (fs.readFileSync(hookPath, "utf-8") === HOOK_BODY) needHook = false } if (needHook) { fs.mkdirSync(path.dirname(hookPath), { recursive: true }) fs.writeFileSync(hookPath, HOOK_BODY, { mode: 0o755 }) steps.push("installed post-commit hook (auto-push)") } const cur = run("git", ["-C", memoryDir, "remote", "get-url", "origin"]) if (cur.status !== 0 || (cur.stdout || "").trim() !== remote) { run("git", ["-C", memoryDir, "remote", "set-url", "origin", remote]) steps.push(`set remote origin: ${remote}`) } } for (const cat of CATEGORIES) { const catDir = path.join(memoryDir, cat) if (!fs.existsSync(catDir)) fs.mkdirSync(catDir, { recursive: true }) } return steps } export default tool({ description: "Commit and re-index all pending memory changes. Call AFTER using Write/Edit tools on ~/.local/share/opencode/opencode-memory/{category}/{filename}.md. " + "Handles: auto-setup (mkdir + git init/clone + hook) on first call, git add -A, commit (message derived from changed files), async RAG re-index (fire-and-forget, log to .rag/reindex.log). " + "WHEN TO SAVE (always save when you discover something reusable): learned non-obvious API pattern/tool quirk/workaround, discovered repo structure/conventions/gotchas, resolved tricky debugging with non-obvious root cause, learned team/ownership info. " + "DO NOT save: one-off answers, things in public docs, context only relevant to the current task. " + "CATEGORIES: preferences, repos, technical, people, workflows, snippets, notes. " + "FRONTMATTER FORMAT: ---\\ntitle: ...\\ntags: [...]\\nsummary: ...\\ncreated: YYYY-MM-DD\\nupdated: YYYY-MM-DD\\nimportance: high|medium|low\\nrelated: [...]\\n---", args: {}, async execute(_args, context) { const memoryDir = resolveMemoryDir() const remote = process.env.OPENCODE_MEMORY_REMOTE const setupSteps = autoSetup(memoryDir, remote) const addR = run("git", ["-C", memoryDir, "add", "-A"]) if (addR.status !== 0) { return `⚠️ memory-save: git add failed (exit ${addR.status}): ${addR.stderr || addR.stdout}` } const diffR = run("git", ["-C", memoryDir, "diff", "--name-only", "--cached"]) if (diffR.status !== 0) { return `⚠️ memory-save: git diff failed (exit ${diffR.status}): ${diffR.stderr || diffR.stdout}` } const changed = (diffR.stdout || "").trim().split("\n").filter(Boolean) if (changed.length === 0) return "No changes to sync" const msg = `memory: sync ${changed.join(", ")}` const commitR = run("git", ["-C", memoryDir, "commit", "-m", msg]) if (commitR.status !== 0) { return `⚠️ memory-save: git commit failed (exit ${commitR.status}): ${commitR.stderr || commitR.stdout}` } if (process.env.OPENAI_BASE_URL) { const indexDir = path.join(memoryDir, ".rag") fs.mkdirSync(indexDir, { recursive: true }) const child = spawn("python3", ["-m", "src.memory", "index", memoryDir, "-o", indexDir], { cwd: context.worktree, stdio: "ignore", detached: true, }) child.unref() } const parts = [`Synced: ${changed.join(", ")}`] if (setupSteps.length > 0) parts.push(`Auto-setup: ${setupSteps.join("; ")}`) if (!process.env.OPENAI_BASE_URL) { parts.push("(semantic index skipped: OPENAI_BASE_URL not set — keyword search still works)") } return parts.join("\n") }, })