// Agent Memory Plugin - Main Entry Point // Uses LanceDB for vector storage with semantic search import { tool } from "@opencode-ai/plugin/tool"; import type { Plugin } from "@opencode-ai/plugin"; import { MemoryStore } from "./store.js"; import { Category, StoreMemoryParams, RecallMemoriesParams, ForgetMemoriesParams, ListMemoriesParams, UpdateMemoryParams } from "./types.js"; // Re-export Plugin type export type { Plugin } from "@opencode-ai/plugin"; // Plugin instance - initialized lazily let store: MemoryStore | null = null; // ============================================================================ // Ollama Check (Auto-start handled by Python CLI) // ============================================================================ const OLLAMA_URL = "http://localhost:11434"; async function checkOllamaRunning(): Promise { try { const response = await fetch(`${OLLAMA_URL}/api/tags`, { method: "GET", signal: AbortSignal.timeout(3000) }); return response.ok; } catch { return false; } } async function ensureOllama(): Promise { // Python CLI handles auto-start, TypeScript just checks status if (await checkOllamaRunning()) { console.log("[agent-memory] ✅ Ollama is running"); return true; } // Python CLI will handle starting Ollama when needed console.log("[agent-memory] ⚠️ Ollama not running. Python CLI will auto-start when needed."); return false; } function getStore(ctx: { worktree?: string }): MemoryStore { if (!store) { store = new MemoryStore({}, ctx); } return store; } // ============================================================================ // Memory Capture - DISABLED (Admin-driven instead) // Auto-capture patterns kept for reference but not active // Admin agent decides when to store important information // ============================================================================ /* // Original patterns kept for reference: // const CAPTURE_PATTERNS = [...] // const SKIP_PATTERNS = [...] // function tryAutoCapture(...) // Not called */ // ============================================================================ // Plugin Definition // ============================================================================ export const AgentMemoryPlugin: Plugin = async (ctx) => { const { client, worktree } = ctx; const log = (...args: any[]) => { const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(" "); console.log("[agent-memory]", msg); }; await log("✅ Plugin initializing..."); // DEFERRED: Check Ollama lazily on first memory operation, not at startup // This prevents blocking OpenCode startup let ollamaChecked = false; let ollamaAvailable = false; const ensureOllamaLazy = async (): Promise => { if (ollamaChecked) return ollamaAvailable; ollamaChecked = true; ollamaAvailable = await ensureOllama(); return ollamaAvailable; }; // Event handler - fixed signature per official docs const handleEvent = async (data: any) => { const type = data?.type || data?.event?.type || "unknown"; // Session events - load memories on new session if (type === "session.created") { try { // Ensure Ollama is available before recall (lazy check) await ensureOllamaLazy(); const memoryStore = getStore({ worktree: ctx.worktree }); const results = await memoryStore.recall({ query: "preferences facts important", limit: 3, }); log("📚 Session started - loaded", results.length, "memories"); } catch (e: any) { log("❌ Session recall error:", e.message); } } // Note: Auto-capture disabled - Admin agent decides what to store }; return { // Custom tools for memory operations tool: { // Debug tool to test if plugin is working memory_debug: tool({ description: "Debug tool to test if agent memory plugin is loaded", args: {}, async execute() { return "Agent Memory plugin is working!"; }, }), // Store a new memory memory_store: tool({ description: "Store a new memory in the agent's long-term memory. Use this to remember important information, preferences, decisions, or facts that should persist across sessions.", args: { text: tool.schema.string().describe("The memory text to store"), category: tool.schema.enum(["preference", "fact", "decision", "entity", "other"]).optional().describe("Category of memory: preference (user preference), fact (factual information), decision (decisions made), entity (people, places, things), other"), scope: tool.schema.string().optional().describe("Memory scope using : convention. Examples: admin:global (default), admin:session-2026-03-05, vault-navigator:global, github-explorer:session-42"), importance: tool.schema.number().min(0).max(1).optional().describe("Importance score 0-1, default 0.5"), metadata: tool.schema.object({}).optional().describe("Optional structured metadata to attach to the memory. Supports any key-value pairs, e.g. note_path, note_title, source_url, tags, session_date, related_ids, created_via"), }, async execute(args) { try { const memoryStore = getStore({ worktree: ctx.worktree }); const params: StoreMemoryParams = { text: args.text, category: args.category as Category || Category.FACT, scope: args.scope || "admin:global", importance: args.importance || 0.5, source: "manual", metadata: args.metadata as Record | undefined, }; const id = await memoryStore.store(params); return `Memory stored successfully!\nID: ${id}\nCategory: ${params.category}\nScope: ${params.scope}`; } catch (error) { return `Error storing memory: ${error instanceof Error ? error.message : String(error)}`; } }, }), // Recall memories memory_recall: tool({ description: "Search memories semantically. Uses vector similarity to find relevant memories based on the query text.", args: { query: tool.schema.string().describe("Search query text"), scope: tool.schema.string().optional().describe("Filter by scope (e.g., 'admin:global')"), category: tool.schema.enum(["preference", "fact", "decision", "entity", "other"]).optional().describe("Filter by category"), limit: tool.schema.number().min(1).max(100).optional().describe("Maximum results to return, default 10"), minImportance: tool.schema.number().min(0).max(1).optional().describe("Minimum importance score 0-1"), }, async execute(args) { try { const memoryStore = getStore({ worktree: ctx.worktree }); const params: RecallMemoriesParams = { query: args.query, scope: args.scope, category: args.category as Category, limit: args.limit || 10, minImportance: args.minImportance || 0.0, }; const results = await memoryStore.recall(params); if (results.length === 0) { return "No memories found matching your query."; } const output = [`Found ${results.length} memory(ies):\n`]; results.forEach((result, i) => { output.push(`${i + 1}. [${result.category}] ${result.text}`); output.push(` Score: ${result.score}% | Scope: ${result.scope} | Importance: ${result.importance}`); output.push(""); }); return output.join("\n"); } catch (error) { return `Error recalling memories: ${error instanceof Error ? error.message : String(error)}`; } }, }), // Forget memories memory_forget: tool({ description: "Delete memories. Provide either a memory ID, a query to search and delete, or a scope to delete all memories in that scope.", args: { memoryId: tool.schema.string().optional().describe("Specific memory ID to delete"), query: tool.schema.string().optional().describe("Search query - will delete matching memories"), scope: tool.schema.string().optional().describe("Delete all memories in this scope"), }, async execute(args) { try { const memoryStore = getStore({ worktree: ctx.worktree }); const params: ForgetMemoriesParams = { memoryId: args.memoryId, query: args.query, scope: args.scope, }; if (!params.memoryId && !params.query && !params.scope) { return "Error: Please provide at least one of: memoryId, query, or scope"; } const deleted = await memoryStore.forget(params); return `Deleted ${deleted} memory(ies)`; } catch (error) { return `Error forgetting memories: ${error instanceof Error ? error.message : String(error)}`; } }, }), // List memories memory_list: tool({ description: "List stored memories without semantic search. Filter by scope or category. IDs are shown for use with memory_forget or memory_update.", args: { scope: tool.schema.string().optional().describe("Filter by scope"), category: tool.schema.enum(["preference", "fact", "decision", "error", "learned", "uncertain"]).optional().describe("Filter by category"), limit: tool.schema.number().min(1).max(100).optional().describe("Maximum results, default 50"), }, async execute(args) { try { const memoryStore = getStore({ worktree: ctx.worktree }); const params: ListMemoriesParams = { scope: args.scope, category: args.category as Category, limit: args.limit || 50, }; const memories = await memoryStore.list(params); if (memories.length === 0) { return "No memories found."; } const output = [`Total: ${memories.length} memories:\n`]; memories.forEach((mem, i) => { output.push(`${i + 1}. [${mem.category}] ${mem.text.substring(0, 80)}${mem.text.length > 80 ? "..." : ""}`); output.push(` ID: ${mem.id} | Scope: ${mem.scope} | Importance: ${mem.importance}`); }); return output.join("\n"); } catch (error) { return `Error listing memories: ${error instanceof Error ? error.message : String(error)}`; } }, }), // Update memory memory_update: tool({ description: "Update an existing memory. Edit text, category, importance, or scope.", args: { memoryId: tool.schema.string().describe("The memory ID to update"), text: tool.schema.string().optional().describe("New memory text (will regenerate embedding)"), category: tool.schema.enum(["preference", "fact", "decision", "error", "learned", "uncertain"]).optional().describe("New category"), importance: tool.schema.number().min(0).max(1).optional().describe("New importance score 0-1"), scope: tool.schema.string().optional().describe("New scope"), }, async execute(args) { try { const memoryStore = getStore({ worktree: ctx.worktree }); const params: UpdateMemoryParams = { memoryId: args.memoryId, text: args.text, category: args.category as Category, importance: args.importance, scope: args.scope, }; const success = await memoryStore.update(params); if (success) { return "Memory updated successfully!"; } else { return "Failed to update memory. Memory may not exist or no changes were specified."; } } catch (error) { return `Error updating memory: ${error instanceof Error ? error.message : String(error)}`; } }, }), // Memory statistics memory_stats: tool({ description: "Get memory system statistics - total count, breakdown by category and scope, time range.", args: {}, async execute() { try { const memoryStore = getStore({ worktree: ctx.worktree }); const stats = await memoryStore.stats(); // Debug: log what we got console.log("[STATS TOOL] Got stats:", JSON.stringify(stats)); const formatDate = (ts?: number) => ts ? new Date(ts).toISOString() : "N/A"; let output = `Memory Statistics\n`; output += `=================\n`; output += `Total memories: ${stats.total}\n\n`; output += `By category:\n`; Object.entries(stats.byCategory).forEach(([cat, count]) => { output += ` ${cat}: ${count}\n`; }); output += `\nBy scope:\n`; Object.entries(stats.byScope).forEach(([scope, count]) => { output += ` ${scope}: ${count}\n`; }); output += `\nTime range: ${formatDate(stats.oldestTimestamp)} to ${formatDate(stats.newestTimestamp)}`; return output; } catch (error) { return `Error getting stats: ${error instanceof Error ? error.message : String(error)}`; } }, }), }, // Event hook - fixed signature per official docs event: async ({ event }) => { if (!event) return; const type = event.type; // Session created - load memories if (type === "session.created") { try { // Ensure Ollama is available before recall (lazy check) await ensureOllamaLazy(); const memoryStore = getStore({ worktree: ctx.worktree }); const results = await memoryStore.recall({ query: "preferences facts important", limit: 3, }); log("📚 Session started - loaded", results.length, "memories"); } catch (e: any) { log("❌ Session recall error:", e.message); } } // Note: Auto-capture disabled - Admin agent decides what to store }, // Chat message hook "chat.message": async (input: any, output: any) => { // Auto-capture disabled - Admin agent decides what to store }, }; }; // Export as 'plugin' to match wakatime plugin pattern export const plugin = AgentMemoryPlugin; export default plugin;