opencode-config/.opencode/tools/memory-save.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

119 lines
No EOL
5.2 KiB
TypeScript

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