/** * MC Plugin — registers /mc as a native slash command in OpenClaw gateway. * Provides interactive commands for the Memory Continuity plugin. */ import fs from "node:fs"; import path from "node:path"; // ── Helpers ───────────────────────────────────────────────────────────── function mono(text) { return "```\n" + text + "\n```"; } const BASE = process.env.OPENCLAW_HOME || path.join(process.env.HOME || "/tmp", ".openclaw"); const MAIN_WS = path.join(BASE, "workspace", "main", "memory"); const EXTRA_WS = path.join(BASE, "workspaces"); /** Discover all agent workspaces that have memory directories */ function discoverAgents() { const agents = []; // Main workspace if (fs.existsSync(path.join(MAIN_WS, "CURRENT_STATE.md"))) { agents.push({ name: "main", memDir: MAIN_WS }); } // Sub-agent workspaces try { for (const d of fs.readdirSync(EXTRA_WS)) { const memDir = path.join(EXTRA_WS, d, "memory"); if (fs.existsSync(path.join(memDir, "CURRENT_STATE.md"))) { agents.push({ name: d, memDir }); } } } catch {} return agents; } function resolveMemDir(agentName) { if (!agentName || agentName === "main") return MAIN_WS; const p = path.join(EXTRA_WS, agentName, "memory"); return fs.existsSync(p) ? p : null; } function readFile(fp) { try { return fs.readFileSync(fp, "utf8"); } catch { return null; } } function extractSection(md, heading) { const re = new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "m"); return (md.match(re)?.[1] ?? "").trim(); } function relTime(dateStr) { try { const diff = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return "just now"; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ${mins % 60}m ago`; return `${Math.floor(hrs / 24)}d ago`; } catch { return dateStr || "unknown"; } } function truncate(s, max = 120) { if (!s) return "(empty)"; const line = s.split("\n")[0]; return line.length > max ? line.slice(0, max) + "…" : line; } // ── Subcommand handlers ───────────────────────────────────────────────── function cmdState(args) { const agent = args || "main"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const md = readFile(path.join(memDir, "CURRENT_STATE.md")); if (!md) return `No state file for "${agent}".`; const updated = md.match(/^> Last updated:\s*(.+)$/m)?.[1]?.trim(); let out = `State: ${agent}\n`; out += `Updated: ${relTime(updated)}\n`; out += "─────────────────────────────\n"; for (const section of ["Objective", "Current Step", "Key Decisions", "Next Action", "Blockers"]) { const val = extractSection(md, section); // Compact multi-line values const compact = val.split("\n").slice(0, 3).join("\n"); out += `${section}:\n ${compact || "(empty)"}\n\n`; } return out.trimEnd(); } function cmdStateAll() { const agents = discoverAgents(); if (!agents.length) return "No agents with memory found."; let out = `Memory States (${agents.length} agents)\n`; out += "─────────────────────────────\n"; for (const { name, memDir } of agents) { const md = readFile(path.join(memDir, "CURRENT_STATE.md")); if (!md) { out += `${name.padEnd(20)} (no state)\n`; continue; } const updated = md.match(/^> Last updated:\s*(.+)$/m)?.[1]?.trim(); const objective = extractSection(md, "Objective"); out += `${name.padEnd(20)} ${relTime(updated).padEnd(10)} ${truncate(objective, 50)}\n`; } return out.trimEnd(); } function cmdHistory(args) { const agent = args || "main"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const archiveDir = path.join(memDir, "session_archive"); let files; try { files = fs.readdirSync(archiveDir).filter(f => f.endsWith(".md")).sort().reverse(); } catch { return `No archive for "${agent}".`; } if (!files.length) return `No archived sessions for "${agent}".`; let out = `Session Archive: ${agent} (${files.length} entries)\n`; out += "─────────────────────────────\n"; out += `${"#".padStart(3)} ${"Timestamp".padEnd(20)} Objective\n`; out += "─".repeat(60) + "\n"; for (let i = 0; i < Math.min(files.length, 20); i++) { const f = files[i]; const ts = f.replace(".md", "").replace(/_/g, " "); const md = readFile(path.join(archiveDir, f)); const obj = md ? truncate(extractSection(md, "Objective"), 35) : "?"; out += `${String(i + 1).padStart(3)} ${ts.padEnd(20)} ${obj}\n`; } if (files.length > 20) out += `\n ... and ${files.length - 20} more`; return out.trimEnd(); } function cmdRestore(args) { const parts = (args || "").trim().split(/\s+/); const idx = parseInt(parts[0]); const agent = parts[1] || "main"; if (isNaN(idx) || idx < 1) return "Usage: /mc restore [agent]\nN = archive number from /mc history"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const archiveDir = path.join(memDir, "session_archive"); let files; try { files = fs.readdirSync(archiveDir).filter(f => f.endsWith(".md")).sort().reverse(); } catch { return `No archive for "${agent}".`; } if (idx > files.length) return `Only ${files.length} archives available.`; const target = files[idx - 1]; const content = readFile(path.join(archiveDir, target)); if (!content) return `Failed to read archive ${target}.`; // Backup current state first const statePath = path.join(memDir, "CURRENT_STATE.md"); const current = readFile(statePath); if (current) { const bak = path.join(memDir, "CURRENT_STATE.md.bak"); fs.writeFileSync(bak, current, "utf8"); } fs.writeFileSync(statePath, content, "utf8"); return `✓ Restored archive #${idx} (${target.replace(".md", "")})\n Previous state backed up to CURRENT_STATE.md.bak`; } function cmdClear(args) { const agent = args || "main"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const statePath = path.join(memDir, "CURRENT_STATE.md"); const current = readFile(statePath); if (current) { // Archive before clearing const archiveDir = path.join(memDir, "session_archive"); fs.mkdirSync(archiveDir, { recursive: true }); const now = new Date(); const pad = (n) => String(n).padStart(2, "0"); const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`; fs.writeFileSync(path.join(archiveDir, `${stamp}.md`), current, "utf8"); } const template = `# Current State > Last updated: ${new Date().toISOString()} ## Objective None ## Current Step None ## Key Decisions - None ## Next Action None ## Blockers None ## Unsurfaced Results None `; fs.writeFileSync(statePath, template, "utf8"); return `✓ State cleared for "${agent}"\n Previous state archived.`; } function cmdSearch(args) { if (!args) return "Usage: /mc search [agent]\nSearches state, archives, sessions, and summaries."; const parts = args.trim().split(/\s+/); let keyword, agent; const agents = discoverAgents(); const agentNames = new Set(agents.map(a => a.name)); if (parts.length > 1 && agentNames.has(parts[parts.length - 1])) { agent = parts.pop(); keyword = parts.join(" "); } else { keyword = parts.join(" "); agent = null; } const re = new RegExp(keyword, "gi"); const results = []; const searchAgents = agent ? [{ name: agent, memDir: resolveMemDir(agent) }] : agents; for (const { name, memDir } of searchAgents) { if (!memDir) continue; // 1. Search current state const state = readFile(path.join(memDir, "CURRENT_STATE.md")); if (state && re.test(state)) { re.lastIndex = 0; const lines = state.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: "CURRENT_STATE", type: "state", matches: lines.slice(0, 3) }); } // 2. Search archives (most recent 30) const archiveDir = path.join(memDir, "session_archive"); try { const files = fs.readdirSync(archiveDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 30)) { re.lastIndex = 0; const content = readFile(path.join(archiveDir, f)); if (content && re.test(content)) { re.lastIndex = 0; const lines = content.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: f.replace(".md", ""), type: "archive", matches: lines.slice(0, 2) }); } } } catch {} // 3. Search session logs (most recent 14 days) const sessionsDir = path.join(memDir, "sessions"); try { const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 14)) { re.lastIndex = 0; const content = readFile(path.join(sessionsDir, f)); if (content && re.test(content)) { re.lastIndex = 0; const lines = content.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: f.replace(".md", ""), type: "session", matches: lines.slice(0, 2) }); } } } catch {} // 4. Search summaries (daily + weekly) for (const sub of ["daily", "weekly"]) { const sumDir = path.join(memDir, "summaries", sub); try { const files = fs.readdirSync(sumDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 10)) { re.lastIndex = 0; const content = readFile(path.join(sumDir, f)); if (content && re.test(content)) { re.lastIndex = 0; const lines = content.split("\n").filter(l => re.test(l)); results.push({ agent: name, source: f.replace(".md", ""), type: sub, matches: lines.slice(0, 2) }); } } } catch {} } } if (!results.length) return `No matches for "${keyword}".`; // Group by type for clearer output let out = `Search: "${keyword}" (${results.length} hits)\n`; out += "─────────────────────────────\n"; const typeOrder = ["state", "archive", "session", "daily", "weekly"]; const typeLabels = { state: "State", archive: "Archive", session: "Session", daily: "Daily", weekly: "Weekly" }; for (const type of typeOrder) { const group = results.filter(r => r.type === type); if (group.length === 0) continue; out += `\n[${typeLabels[type]}]\n`; for (const r of group.slice(0, 5)) { out += ` ${r.agent}/${r.source}\n`; for (const line of r.matches) { out += ` ${truncate(line.trim(), 75)}\n`; } } if (group.length > 5) out += ` ... +${group.length - 5} more\n`; } return out.trimEnd(); } function cmdSettings(args) { // Read memory-continuity config from openclaw.json const configPath = path.join(BASE, "openclaw.json"); const raw = readFile(configPath); if (!raw) return "Cannot read openclaw.json"; let config; try { config = JSON.parse(raw); } catch { return "Cannot parse openclaw.json"; } const mcConfig = config?.plugins?.entries?.["memory-continuity"]?.config || config?.entries?.["memory-continuity"]?.config || {}; if (!args) { const defaults = { maxStateLines: { value: mcConfig.maxStateLines ?? 50, desc: "Max lines before compress warning" }, archiveOnNew: { value: mcConfig.archiveOnNew ?? true, desc: "Archive state on /new" }, autoExtract: { value: mcConfig.autoExtract ?? true, desc: "Auto-extract state at agent_end" }, maxArchiveCount: { value: mcConfig.maxArchiveCount ?? 20, desc: "Max archive files kept" }, }; let out = "MC Settings\n─────────────────────────────\n"; for (const [k, v] of Object.entries(defaults)) { out += `${k.padEnd(18)} ${String(v.value).padStart(6)} ${v.desc}\n`; } // Show stats const agents = discoverAgents(); out += "\nStats:\n"; out += ` Agents with memory: ${agents.length}\n`; let totalArchives = 0; for (const { memDir } of agents) { try { totalArchives += fs.readdirSync(path.join(memDir, "session_archive")).filter(f => f.endsWith(".md")).length; } catch {} } out += ` Total archives: ${totalArchives}\n`; return out.trimEnd(); } // Parse "key value" for settings update const parts = args.trim().split(/\s+/); if (parts.length < 2 || parts[0] === "--help" || parts[0] === "-h") { return "Usage: /mc settings \nKeys: maxStateLines, archiveOnNew, autoExtract, maxArchiveCount"; } const [key, val] = parts; const validKeys = ["maxStateLines", "archiveOnNew", "autoExtract", "maxArchiveCount"]; if (!validKeys.includes(key)) return `Unknown key: ${key}\nValid: ${validKeys.join(", ")}`; let parsed; if (val === "true") parsed = true; else if (val === "false") parsed = false; else if (!isNaN(Number(val))) parsed = Number(val); else return `Cannot parse value: ${val}`; // Update openclaw.json try { // Ensure path exists if (!config.entries) config.entries = {}; if (!config.entries["memory-continuity"]) config.entries["memory-continuity"] = { enabled: true }; if (!config.entries["memory-continuity"].config) config.entries["memory-continuity"].config = {}; config.entries["memory-continuity"].config[key] = parsed; fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8"); return `✓ ${key} = ${parsed}\n Restart gateway to apply.`; } catch (e) { return `✗ Failed to write config: ${e.message}`; } } function cmdCompact(args) { const agent = args || "main"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const statePath = path.join(memDir, "CURRENT_STATE.md"); const md = readFile(statePath); if (!md) return `No state for "${agent}".`; const lines = md.split("\n"); const originalLen = lines.length; // Keep headers and first meaningful line of each section const compacted = []; let inSection = false; let sectionLines = 0; for (const line of lines) { if (line.startsWith("# ") || line.startsWith("> Last updated")) { compacted.push(line); inSection = false; } else if (line.startsWith("## ")) { compacted.push(line); inSection = true; sectionLines = 0; } else if (inSection) { sectionLines++; if (sectionLines <= 3 || line.startsWith("- ")) { compacted.push(line); } } else { compacted.push(line); } } // Update timestamp const result = compacted.join("\n") .replace(/^> Last updated:.*$/m, `> Last updated: ${new Date().toISOString()}`); fs.writeFileSync(statePath, result, "utf8"); return `✓ Compacted "${agent}" state: ${originalLen} → ${compacted.length} lines`; } function cmdExport(args) { const parts = (args || "").trim().split(/\s+/); // Parse args: [agent|all] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--tag #tag] let agent = "all"; let fromDate = null; let toDate = null; let filterTag = null; for (let i = 0; i < parts.length; i++) { if (parts[i] === "--from" && parts[i + 1]) { fromDate = parts[++i]; continue; } if (parts[i] === "--to" && parts[i + 1]) { toDate = parts[++i]; continue; } if (parts[i] === "--tag" && parts[i + 1]) { filterTag = parts[++i].toLowerCase(); continue; } if (parts[i] && !parts[i].startsWith("-")) agent = parts[i]; } const agents = agent === "all" ? discoverAgents() : [{ name: agent, memDir: resolveMemDir(agent) }]; if (agents.length === 0 || !agents[0]?.memDir) return `Agent "${agent}" not found.`; const exportDir = path.join(BASE, "exports"); fs.mkdirSync(exportDir, { recursive: true }); const now = new Date(); const pad = (n) => String(n).padStart(2, "0"); const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`; const exportFile = path.join(exportDir, `mc-export-${stamp}.md`); let content = `# Memory Continuity Export\n> Exported: ${now.toISOString()}\n`; if (fromDate || toDate) content += `> Date range: ${fromDate || "start"} to ${toDate || "now"}\n`; if (filterTag) content += `> Tag filter: ${filterTag}\n`; content += "\n"; const matchesDateRange = (filename) => { if (!fromDate && !toDate) return true; const dateMatch = filename.match(/(\d{4}-\d{2}-\d{2})/); if (!dateMatch) return true; // Include files without dates const fileDate = dateMatch[1]; if (fromDate && fileDate < fromDate) return false; if (toDate && fileDate > toDate) return false; return true; }; const matchesTag = (text) => { if (!filterTag) return true; return text.toLowerCase().includes(filterTag); }; for (const { name, memDir } of agents) { if (!memDir) continue; content += `---\n# Agent: ${name}\n\n`; // Current state const state = readFile(path.join(memDir, "CURRENT_STATE.md")); if (state && matchesTag(state)) { content += `## Current State\n\n${state}\n\n`; } // Archives (filtered) const archiveDir = path.join(memDir, "session_archive"); try { const files = fs.readdirSync(archiveDir).filter(f => f.endsWith(".md") && matchesDateRange(f)).sort().reverse(); if (files.length) { content += `## Archives (${files.length})\n\n`; for (const f of files) { const ac = readFile(path.join(archiveDir, f)); if (ac && matchesTag(ac)) content += `### ${f.replace(".md", "")}\n\n${ac}\n\n`; } } } catch {} // Session logs (filtered) const sessionsDir = path.join(memDir, "sessions"); try { const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".md") && matchesDateRange(f)).sort().reverse(); if (files.length) { content += `## Session Logs (${files.length} days)\n\n`; for (const f of files) { const sc = readFile(path.join(sessionsDir, f)); if (sc && matchesTag(sc)) content += `### ${f.replace(".md", "")}\n\n${sc}\n\n`; } } } catch {} // Summaries (filtered) for (const sub of ["daily", "weekly"]) { const sumDir = path.join(memDir, "summaries", sub); try { const files = fs.readdirSync(sumDir).filter(f => f.endsWith(".md") && matchesDateRange(f)).sort().reverse(); if (files.length) { content += `## ${sub.charAt(0).toUpperCase() + sub.slice(1)} Summaries (${files.length})\n\n`; for (const f of files) { const sm = readFile(path.join(sumDir, f)); if (sm && matchesTag(sm)) content += `### ${f.replace(".md", "")}\n\n${sm}\n\n`; } } } catch {} } } fs.writeFileSync(exportFile, content, "utf8"); const sizeKB = (Buffer.byteLength(content) / 1024).toFixed(1); return `✓ Exported to:\n ${exportFile}\n ${agents.length} agent(s), ${sizeKB} KB`; } function cmdSessions(args) { const parts = (args || "").trim().split(/\s+/); const agent = parts.find(p => !p.startsWith("-") && !p.startsWith("2")) || "main"; const dateArg = parts.find(p => /^\d{4}-\d{2}-\d{2}$/.test(p)); const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const sessionsDir = path.join(memDir, "sessions"); let files; try { files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".md")).sort().reverse(); } catch { return `No session logs for "${agent}".`; } if (!files.length) return `No session logs for "${agent}".`; // If date specified, show that day's log if (dateArg) { const target = `${dateArg}.md`; const content = readFile(path.join(sessionsDir, target)); if (!content) return `No session log for ${dateArg}.`; // Truncate to last 50 lines to stay compact const lines = content.split("\n"); const shown = lines.length > 50 ? lines.slice(-50) : lines; let out = shown.join("\n"); if (lines.length > 50) out = `... (${lines.length - 50} earlier lines omitted)\n\n` + out; return out; } // List recent session logs let out = `Session Logs: ${agent} (${files.length} days)\n`; out += "─────────────────────────────\n"; for (const f of files.slice(0, 14)) { const date = f.replace(".md", ""); const content = readFile(path.join(sessionsDir, f)); // Count session entries (### HH:MM headers) const sessionCount = content ? (content.match(/^### \d{2}:\d{2}/gm) || []).length : 0; out += `${date} ${String(sessionCount).padStart(3)} session(s)\n`; } if (files.length > 14) out += `\n ... and ${files.length - 14} more days`; out += `\n\nUse /mc sessions to view a specific day.`; return out.trimEnd(); } function cmdTags(args) { const agent = args || "main"; const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; const tagsFile = path.join(memDir, "tags.md"); const content = readFile(tagsFile); if (!content) return `No tags for "${agent}".`; // Parse tags and their date counts const tagSections = content.split(/^## /gm).filter(s => s.trim()); if (tagSections.length === 0) return `No tags for "${agent}".`; let out = `Tags: ${agent} (${tagSections.length} tags)\n`; out += "─────────────────────────────\n"; const tags = []; for (const section of tagSections) { const lines = section.trim().split("\n"); const tag = lines[0].trim(); if (!tag.startsWith("#")) continue; const dates = lines.filter(l => l.startsWith("- ")); tags.push({ tag, count: dates.length, latest: dates[0]?.replace("- ", "") || "?" }); } // Sort by count descending tags.sort((a, b) => b.count - a.count); for (const { tag, count, latest } of tags.slice(0, 30)) { out += `${tag.padEnd(25)} ${String(count).padStart(3)} days latest: ${latest}\n`; } if (tags.length > 30) out += `\n ... and ${tags.length - 30} more tags`; return out.trimEnd(); } function cmdSummary(args) { const parts = (args || "").trim().split(/\s+/); const agent = parts.find(p => !p.startsWith("-") && !/^\d{4}/.test(p) && p !== "daily" && p !== "weekly") || "main"; const typeArg = parts.find(p => p === "daily" || p === "weekly"); const dateArg = parts.find(p => /^\d{4}/.test(p)); const memDir = resolveMemDir(agent); if (!memDir) return `Agent "${agent}" not found.`; // If specific date given, show that summary if (dateArg) { const type = typeArg || (dateArg.includes("W") ? "weekly" : "daily"); const sumFile = path.join(memDir, "summaries", type, `${dateArg}.md`); const content = readFile(sumFile); if (!content) return `No ${type} summary for ${dateArg}.`; return content; } // List available summaries const types = typeArg ? [typeArg] : ["daily", "weekly"]; let out = `Summaries: ${agent}\n`; out += "─────────────────────────────\n"; for (const type of types) { const sumDir = path.join(memDir, "summaries", type); let files; try { files = fs.readdirSync(sumDir).filter(f => f.endsWith(".md")).sort().reverse(); } catch { continue; } if (files.length === 0) continue; out += `\n[${type.charAt(0).toUpperCase() + type.slice(1)}] (${files.length})\n`; for (const f of files.slice(0, 10)) { const name = f.replace(".md", ""); const content = readFile(path.join(sumDir, f)); const sessMatch = content?.match(/\*\*(?:Total s|S)essions:\*\*\s*(\d+)/); const sessions = sessMatch ? sessMatch[1] : "?"; out += ` ${name} ${sessions} session(s)\n`; } if (files.length > 10) out += ` ... +${files.length - 10} more\n`; } out += `\nUse /mc summary or /mc summary to view details.`; return out.trimEnd(); } function cmdRecall(args) { if (!args) return "Usage: /mc recall \nSearches history and returns the most relevant entries for context injection."; const topic = args.trim(); const agents = discoverAgents(); if (agents.length === 0) return "No agents with memory found."; // Search across all agents' session logs and summaries const words = topic .replace(/[^\w\u4e00-\u9fff\u3400-\u4dbf-]/g, " ") .split(/\s+/) .filter(w => w.length > 2) .map(w => w.toLowerCase()); if (words.length === 0) return "Need more specific topic keywords."; const scored = []; for (const { name, memDir } of agents) { if (!memDir) continue; // Search session logs const sessionsDir = path.join(memDir, "sessions"); try { const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 14)) { const content = readFile(path.join(sessionsDir, f)); if (!content) continue; const entries = content.split(/^### /gm).filter(e => e.trim()); for (const entry of entries) { let score = 0; const lower = entry.toLowerCase(); for (const w of words) { if (lower.includes(w)) score++; } if (score >= 2) { const topicMatch = entry.match(/\*\*Topic:\*\*\s*(.+)/); const time = entry.match(/^(\d{2}:\d{2})/)?.[1] || ""; scored.push({ agent: name, date: `${f.replace(".md", "")} ${time}`, score, text: topicMatch?.[1] || entry.split("\n")[0].slice(0, 80) }); } } } } catch {} // Search daily summaries const dailyDir = path.join(memDir, "summaries", "daily"); try { const files = fs.readdirSync(dailyDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 14)) { const content = readFile(path.join(dailyDir, f)); if (!content) continue; let score = 0; const lower = content.toLowerCase(); for (const w of words) { if (lower.includes(w)) score++; } if (score >= 2) { const topicsMatch = content.match(/## (?:Key )?Topics\n([\s\S]*?)(?:\n##|$)/); const topics = topicsMatch ? topicsMatch[1].trim().split("\n").slice(0, 3).join("; ") : ""; scored.push({ agent: name, date: f.replace(".md", ""), score, text: topics || "(daily summary)" }); } } } catch {} // Search archives const archiveDir = path.join(memDir, "session_archive"); try { const files = fs.readdirSync(archiveDir).filter(f => f.endsWith(".md")).sort().reverse(); for (const f of files.slice(0, 20)) { const content = readFile(path.join(archiveDir, f)); if (!content) continue; let score = 0; const lower = content.toLowerCase(); for (const w of words) { if (lower.includes(w)) score++; } if (score >= 2) { const objMatch = content.match(/## Objective\n(.+)/); scored.push({ agent: name, date: f.replace(".md", "").replace(/_/g, " "), score, text: objMatch?.[1]?.slice(0, 80) || "(archived state)" }); } } } catch {} } if (scored.length === 0) return `No relevant history found for "${topic}".`; scored.sort((a, b) => b.score - a.score); const top = scored.slice(0, 10); let out = `Recall: "${topic}" (${scored.length} matches, showing top ${top.length})\n`; out += "─────────────────────────────\n"; for (const item of top) { out += `[${item.agent}] ${item.date} (score: ${item.score})\n`; out += ` ${truncate(item.text, 80)}\n\n`; } return out.trimEnd(); } function cmdHelp() { return `MC Commands (Memory Continuity) ───────────────────────────── /mc state [agent] View current state (default: main) /mc state --all Overview of all agents /mc sessions [date] Session logs (daily activity) /mc summary [daily|weekly] List or view summaries /mc tags [agent] View tag index /mc history [agent] List archived sessions /mc restore [agent] Restore archive #N /mc clear [agent] Clear state (archives first) /mc search Search across all memory /mc settings View MC settings /mc settings Update a setting /mc compact [agent] Compress state file /mc export [agent|all] Export (--from/--to/--tag) /mc recall Find relevant history by topic`; } // ── Plugin entry point ────────────────────────────────────────────────── export default function (api) { console.log("[mc] MC plugin loading, registering /mc command..."); api.registerCommand({ name: "mc", description: "Memory Continuity commands — state, history, search, settings, etc.", acceptsArgs: true, requireAuth: true, handler: async (ctx) => { const raw = (ctx.args || "").trim(); const spaceIdx = raw.indexOf(" "); const subcmd = spaceIdx === -1 ? raw : raw.slice(0, spaceIdx); const subargs = spaceIdx === -1 ? "" : raw.slice(spaceIdx + 1).trim(); try { let text; switch (subcmd) { case "state": text = subargs === "--all" ? cmdStateAll() : cmdState(subargs || null); break; case "sessions": text = cmdSessions(subargs || null); break; case "history": text = cmdHistory(subargs || null); break; case "restore": text = cmdRestore(subargs); break; case "clear": text = cmdClear(subargs || null); break; case "search": text = cmdSearch(subargs); break; case "settings": text = cmdSettings(subargs || null); break; case "compact": text = cmdCompact(subargs || null); break; case "export": text = cmdExport(subargs || null); break; case "tags": text = cmdTags(subargs || null); break; case "summary": text = cmdSummary(subargs || null); break; case "recall": text = cmdRecall(subargs); break; case "help": case "--help": case "-h": case "": text = cmdHelp(); break; default: text = `Unknown subcommand: ${subcmd}\n\n${cmdHelp()}`; } return { text: mono(text) }; } catch (err) { return { text: `MC error: ${err.message}` }; } }, }); }