opencode-config/.opencode/tools/_memory-shared.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

137 lines
No EOL
4.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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 { createRequire } from "node:module"
import fs from "fs"
import os from "os"
import path from "path"
const require2 = createRequire(import.meta.url)
/**
* Resolve the memory directory from OPENCODE_MEMORY_DIR env var or fall back
* to the default ~/.local/share/opencode/opencode-memory. Identical behaviour
* across all 5 memory tools — previously duplicated 5×.
*/
export function resolveMemoryDir(): string {
const env = process.env.OPENCODE_MEMORY_DIR
if (env && env.length > 0) return path.normalize(env)
return path.join(os.homedir(), ".local/share/opencode/opencode-memory")
}
/**
* Memory categories (also the directories auto-created by memory-save).
* Previously duplicated in memory-save and memory-list.
*/
export const CATEGORIES = [
"preferences",
"repos",
"technical",
"people",
"workflows",
"snippets",
"notes",
]
/**
* Frontmatter delimiter regex (flat YAML between --- fences). Previously
* duplicated in memory-search, memory-list, memory-access.
*/
export const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/
export interface FrontmatterMeta {
title?: string
summary?: string
importance?: string
updated?: string
tags?: string[]
related?: string[]
access_count?: number
}
/**
* Parse flat frontmatter (regex per line, NOT real YAML — see memory note
* "Frontmatter parser is regex, not YAML"). Returns typed meta; callers
* ignore fields they don't use. Previously duplicated in memory-search (full)
* and memory-list (partial) — unified to the full version.
*/
export function parseFrontmatter(content: string): FrontmatterMeta {
const match = content.match(FRONTMATTER_RE)
if (!match) return {}
const yaml = match[1]
const meta: FrontmatterMeta = {}
for (const line of yaml.split("\n")) {
const kv = line.match(/^(\w+):\s*(.+)$/)
if (!kv) continue
const [, key, rawValue] = kv
const value = rawValue.trim()
if (key === "title") meta.title = value
else if (key === "summary") meta.summary = value
else if (key === "importance") meta.importance = value
else if (key === "updated") meta.updated = value
else if (key === "access_count") {
const parsed = parseInt(value, 10)
meta.access_count = Number.isFinite(parsed) ? parsed : 0
} else if (key === "tags" || key === "related") {
const arr = value.match(/\[([^\]]*)\]/)
if (arr && arr[1].trim()) {
meta[key] = arr[1].split(",").map((t) => t.trim()).filter(Boolean)
} else {
meta[key] = []
}
}
}
return meta
}
/**
* Resolve the ripgrep binary path. Checks @vscode/ripgrep npm package first
* (platform-specific binary). When `allowSystemFallback` is true (memory-search),
* falls back to a system `rg` on $PATH. When false (memory-doctor), returns
* null if the npm package is unavailable so the doctor can report the two
* cases separately. Previously duplicated with divergent behaviour.
*/
export function resolveRgBinary(opts?: { allowSystemFallback?: boolean }): string | null {
try {
const mod = require2("@vscode/ripgrep")
if (typeof mod.rgPath === "string" && mod.rgPath.length > 0 && fs.existsSync(mod.rgPath)) {
return mod.rgPath
}
} catch (e) {
process.stderr.write(`[memory] @vscode/ripgrep require failed: ${(e as Error).message}\n`)
}
if (opts?.allowSystemFallback) {
const sys = spawnSync("rg", ["--version"], { encoding: "utf-8" })
if (sys.status === 0) return "rg"
}
return null
}
/**
* Recursively collect absolute paths of *.md files under `dir`, skipping
* dotfiles, .git/, .rag/, and INDEX.md. Previously duplicated in memory-list
* (collecting relative paths) and memory-doctor (counting only); unified to
* return absolute paths — callers convert with path.relative() as needed.
*/
export function walkMd(dir: string): string[] {
const out: string[] = []
const walk = (d: string) => {
let entries: fs.Dirent[]
try {
entries = fs.readdirSync(d, { withFileTypes: true })
} catch (e) {
process.stderr.write(`[memory] walkMd readdir failed on ${d}: ${(e as Error).message}\n`)
return
}
for (const e of entries) {
if (e.name.startsWith(".")) continue
const full = path.join(d, e.name)
if (e.isDirectory()) {
if (e.name === ".git" || e.name === ".rag") continue
walk(full)
} else if (e.isFile() && e.name.endsWith(".md") && e.name !== "INDEX.md") {
out.push(full)
}
}
}
walk(dir)
return out
}