/** * Moltbot Memory (PowerMem) Plugin * * Long-term memory via PowerMem HTTP API: intelligent extraction, * Ebbinghaus forgetting curve, multi-agent isolation. Requires a running * PowerMem server (e.g. powermem-server --port 8000). */ import { Type } from "@sinclair/typebox"; import type { MoltbotPluginApi } from "clawdbot/plugin-sdk"; import { powerMemConfigSchema, resolveUserId, resolveAgentId, type PowerMemConfig, } from "./config.js"; import { PowerMemClient } from "./client.js"; // ============================================================================ // Rule-based capture filter (same idea as memory-lancedb) // ============================================================================ const MEMORY_TRIGGERS = [ /zapamatuj si|pamatuj|remember/i, /preferuji|radši|nechci|prefer/i, /rozhodli jsme|budeme používat/i, /\+\d{10,}/, /[\w.-]+@[\w.-]+\.\w+/, /můj\s+\w+\s+je|je\s+můj/i, /my\s+\w+\s+is|is\s+my/i, /i (like|prefer|hate|love|want|need)/i, /always|never|important/i, ]; function shouldCapture(text: string): boolean { if (text.length < 10 || text.length > 500) return false; if (text.includes("")) return false; if (text.startsWith("<") && text.includes(" 3) return false; return MEMORY_TRIGGERS.some((r) => r.test(text)); } // ============================================================================ // Plugin Definition // ============================================================================ const memoryPlugin = { id: "memory-powermem", name: "Memory (PowerMem)", description: "PowerMem-backed long-term memory (intelligent extraction, forgetting curve). Requires PowerMem server.", kind: "memory" as const, configSchema: powerMemConfigSchema, register(api: MoltbotPluginApi) { const cfg = powerMemConfigSchema.parse(api.pluginConfig) as PowerMemConfig; const userId = resolveUserId(cfg); const agentId = resolveAgentId(cfg); const client = PowerMemClient.fromConfig(cfg, userId, agentId); api.logger.info?.( `memory-powermem: plugin registered (baseUrl: ${cfg.baseUrl}, user: ${userId}, agent: ${agentId})`, ); // ======================================================================== // Tools // ======================================================================== api.registerTool( { name: "memory_recall", label: "Memory Recall", description: "Search through long-term memories. Use when you need context about user preferences, past decisions, or previously discussed topics.", parameters: Type.Object({ query: Type.String({ description: "Search query" }), limit: Type.Optional(Type.Number({ description: "Max results (default: 5)" })), }), async execute(_toolCallId, params) { const { query, limit = 5 } = params as { query: string; limit?: number }; try { const results = await client.search(query, limit); if (results.length === 0) { return { content: [{ type: "text", text: "No relevant memories found." }], details: { count: 0 }, }; } const text = results .map( (r, i) => `${i + 1}. ${r.content} (${((r.score ?? 0) * 100).toFixed(0)}%)`, ) .join("\n"); const sanitizedResults = results.map((r) => ({ id: String(r.memory_id), text: r.content, score: r.score, })); return { content: [ { type: "text", text: `Found ${results.length} memories:\n\n${text}` }, ], details: { count: results.length, memories: sanitizedResults }, }; } catch (err) { api.logger.warn?.(`memory-powermem: recall failed: ${String(err)}`); return { content: [ { type: "text", text: `Memory search failed: ${err instanceof Error ? err.message : String(err)}`, }, ], details: { error: String(err) }, }; } }, }, { name: "memory_recall" }, ); api.registerTool( { name: "memory_store", label: "Memory Store", description: "Save important information in long-term memory. Use for preferences, facts, decisions.", parameters: Type.Object({ text: Type.String({ description: "Information to remember" }), importance: Type.Optional( Type.Number({ description: "Importance 0-1 (default: 0.7)" }), ), }), async execute(_toolCallId, params) { const { text, importance = 0.7 } = params as { text: string; importance?: number; }; try { const created = await client.add(text, { infer: cfg.inferOnAdd, metadata: { importance }, }); if (created.length === 0) { return { content: [{ type: "text", text: "Stored (no inferred items)." }], details: { action: "created" }, }; } const summary = created.length === 1 ? created[0].content.slice(0, 80) : `${created.length} items stored`; return { content: [ { type: "text", text: `Stored: ${summary}${summary.length >= 80 ? "..." : ""}` }, ], details: { action: "created", count: created.length, ids: created.map((c) => String(c.memory_id)), }, }; } catch (err) { api.logger.warn?.(`memory-powermem: store failed: ${String(err)}`); return { content: [ { type: "text", text: `Failed to store memory: ${err instanceof Error ? err.message : String(err)}`, }, ], details: { error: String(err) }, }; } }, }, { name: "memory_store" }, ); api.registerTool( { name: "memory_forget", label: "Memory Forget", description: "Delete specific memories. GDPR-compliant.", parameters: Type.Object({ query: Type.Optional(Type.String({ description: "Search to find memory" })), memoryId: Type.Optional(Type.String({ description: "Specific memory ID" })), }), async execute(_toolCallId, params) { const { query, memoryId } = params as { query?: string; memoryId?: string }; try { if (memoryId) { await client.delete(memoryId); return { content: [{ type: "text", text: `Memory ${memoryId} forgotten.` }], details: { action: "deleted", id: memoryId }, }; } if (query) { const results = await client.search(query, 5); if (results.length === 0) { return { content: [{ type: "text", text: "No matching memories found." }], details: { found: 0 }, }; } if (results.length === 1 && (results[0].score ?? 0) > 0.9) { await client.delete(results[0].memory_id); return { content: [ { type: "text", text: `Forgotten: "${results[0].content.slice(0, 60)}..."`, }, ], details: { action: "deleted", id: String(results[0].memory_id) }, }; } const list = results .map( (r) => `- [${String(r.memory_id).slice(0, 8)}] ${r.content.slice(0, 60)}...`, ) .join("\n"); return { content: [ { type: "text", text: `Found ${results.length} candidates. Specify memoryId:\n${list}`, }, ], details: { action: "candidates", candidates: results.map((r) => ({ id: String(r.memory_id), text: r.content, score: r.score, })), }, }; } return { content: [{ type: "text", text: "Provide query or memoryId." }], details: { error: "missing_param" }, }; } catch (err) { api.logger.warn?.(`memory-powermem: forget failed: ${String(err)}`); return { content: [ { type: "text", text: `Failed to forget: ${err instanceof Error ? err.message : String(err)}`, }, ], details: { error: String(err) }, }; } }, }, { name: "memory_forget" }, ); // ======================================================================== // CLI Commands // ======================================================================== api.registerCli( ({ program }) => { const ltm = program .command("ltm") .description("PowerMem long-term memory plugin commands"); ltm .command("search") .description("Search memories") .argument("", "Search query") .option("--limit ", "Max results", "5") .action(async (query: string, opts: { limit?: string }) => { const limit = parseInt(opts.limit ?? "5", 10); const results = await client.search(query, limit); console.log(JSON.stringify(results, null, 2)); }); ltm .command("health") .description("Check PowerMem server health") .action(async () => { try { const h = await client.health(); console.log("PowerMem:", h.status); } catch (err) { console.error("PowerMem health check failed:", err); process.exitCode = 1; } }); }, { commands: ["ltm"] }, ); // ======================================================================== // Lifecycle Hooks // ======================================================================== if (cfg.autoRecall) { api.on("before_agent_start", async (event) => { if (!event.prompt || event.prompt.length < 5) return; try { const results = await client.search(event.prompt, 3); if (results.length === 0) return; const memoryContext = results.map((r) => `- ${r.content}`).join("\n"); api.logger.info?.( `memory-powermem: injecting ${results.length} memories into context`, ); return { prependContext: `\nThe following memories may be relevant to this conversation:\n${memoryContext}\n`, }; } catch (err) { api.logger.warn?.(`memory-powermem: recall failed: ${String(err)}`); } }); } if (cfg.autoCapture) { api.on("agent_end", async (event) => { if (!event.success || !event.messages || event.messages.length === 0) { return; } try { const texts: string[] = []; for (const msg of event.messages) { if (!msg || typeof msg !== "object") continue; const msgObj = msg as Record; const role = msgObj.role; if (role !== "user" && role !== "assistant") continue; const content = msgObj.content; if (typeof content === "string") { texts.push(content); continue; } if (Array.isArray(content)) { for (const block of content) { if ( block && typeof block === "object" && "type" in block && (block as Record).type === "text" && "text" in block && typeof (block as Record).text === "string" ) { texts.push((block as Record).text as string); } } } } const toCapture = texts.filter((t) => t && shouldCapture(t)); if (toCapture.length === 0) return; let stored = 0; for (const text of toCapture.slice(0, 3)) { const created = await client.add(text, { infer: cfg.inferOnAdd }); stored += created.length; } if (stored > 0) { api.logger.info?.(`memory-powermem: auto-captured ${stored} memories`); } } catch (err) { api.logger.warn?.(`memory-powermem: capture failed: ${String(err)}`); } }); } // ======================================================================== // Service // ======================================================================== api.registerService({ id: "memory-powermem", start: async () => { try { const h = await client.health(); api.logger.info?.( `memory-powermem: initialized (${cfg.baseUrl}, health: ${h.status})`, ); } catch (err) { api.logger.warn?.( `memory-powermem: health check failed (is PowerMem server running?): ${String(err)}`, ); } }, stop: () => { api.logger.info?.("memory-powermem: stopped"); }, }); }, }; export default memoryPlugin;