toon-memory 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toon-memory",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Persistent memory system for OpenCode AI agent using TOON format (40% fewer tokens than JSON)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,113 @@
1
+ # toon-memory — Persistent Memory for OpenCode
2
+
3
+ ## What is this?
4
+
5
+ A persistent memory system for the OpenCode AI agent. It remembers decisions, patterns, bugs, and knowledge **between sessions** using TOON format (40% fewer tokens than JSON).
6
+
7
+ ## Tools
8
+
9
+ | Tool | Description |
10
+ |------|-------------|
11
+ | `memory_remember` | Save a decision, pattern, bug, or knowledge |
12
+ | `memory_recall` | Search memory (use BEFORE reading files) |
13
+ | `memory_forget` | Remove an entry by key or id |
14
+ | `memory_stats` | View memory state |
15
+ | `memory_summary` | Save/retrieve file summaries |
16
+
17
+ ## Installation
18
+
19
+ ### Option 1: Interactive installer (recommended)
20
+
21
+ ```bash
22
+ npx toon-memory
23
+ ```
24
+
25
+ This will:
26
+ 1. Detect installed agents (OpenCode, Claude, Cursor)
27
+ 2. Ask if you want local or global installation
28
+ 3. Configure the MCP server automatically
29
+
30
+ ### Option 2: Global MCP Server
31
+
32
+ Add to `~/.config/opencode/opencode.json`:
33
+
34
+ ```json
35
+ {
36
+ "mcp": {
37
+ "toon-memory": {
38
+ "type": "local",
39
+ "command": ["npx", "-y", "toon-memory", "mcp"],
40
+ "enabled": true
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Option 3: Project-level MCP Server
47
+
48
+ Add to `.opencode/opencode.json`:
49
+
50
+ ```json
51
+ {
52
+ "mcp": {
53
+ "toon-memory": {
54
+ "type": "local",
55
+ "command": ["npx", "-y", "toon-memory", "mcp"],
56
+ "enabled": true
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ## How to use
63
+
64
+ ### At the START of every session
65
+
66
+ 1. Run `memory_stats` to see what's in memory.
67
+ 2. If the user asks something that might be in memory → `memory_recall(query)` BEFORE reading files.
68
+
69
+ ### When making decisions
70
+
71
+ - Before implementing a non-trivial change → `memory_remember(category='decision')`
72
+ - When you resolve a complex bug → `memory_remember(category='bug')`
73
+ - When you observe a code pattern → `memory_remember(category='pattern')`
74
+
75
+ ### When exploring the project
76
+
77
+ 1. First: `memory_recall(query="what you need to know")`
78
+ 2. If no result: use CodeGraph or grep
79
+ 3. Only if both fail: read files directly
80
+
81
+ ### At the END of every session
82
+
83
+ 1. If you made design decisions → `memory_remember(category='decision')`
84
+ 2. If you resolved a bug → `memory_remember(category='bug')`
85
+ 3. If you observed a pattern → `memory_remember(category='pattern')`
86
+
87
+ ## Categories
88
+
89
+ | Category | When | Example |
90
+ |----------|------|---------|
91
+ | `decision` | Non-trivial design decision | "Use pandas instead of numpy for time series" |
92
+ | `pattern` | Observed code pattern | "This project uses Pydantic v2 for configs" |
93
+ | `bug` | Complex bug resolved | "Redis pool exhaustion, missing max_connections" |
94
+ | `knowledge` | General project knowledge | "Broker uses RESP protocol, not Redis-specific" |
95
+
96
+ ## File format
97
+
98
+ Data is stored in `.opencode/memory/data.toon` using TOON (Token-Oriented Object Notation):
99
+
100
+ ```
101
+ version: 1
102
+ entries[2|]{id|category|key|content|file|tags|date}:
103
+ a1b2c3d4|decision|risk-priority|Risk Engine has priority|spec.md:145|risk;spec|2026-07-10
104
+ e5f6g7h8|pattern|pandas-over-numpy|Analytics uses pandas|indicators.py|python;analytics|2026-07-10
105
+ summaries:
106
+ path/to/big-file.py: Brief summary of what this file does
107
+ ```
108
+
109
+ ## Anti-patterns
110
+
111
+ - Don't store trivia in memory (only important facts)
112
+ - Don't forget to save a decision before implementing
113
+ - Don't re-read files that already have a summary in `memory_summary`
@@ -0,0 +1,251 @@
1
+ import { McpServer } from "@modelcontextprotocol/server"
2
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
3
+ import { z } from "zod"
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
5
+ import { dirname, join } from "path"
6
+ import { fileURLToPath } from "url"
7
+ import { randomBytes } from "crypto"
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url))
10
+ const MEMORY_DIR = join(process.cwd(), ".opencode", "memory")
11
+ const MEMORY_FILE = join(MEMORY_DIR, "data.toon")
12
+
13
+ function ensureMemoryFile() {
14
+ if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
15
+ if (!existsSync(MEMORY_FILE)) {
16
+ writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
17
+ }
18
+ }
19
+
20
+ function readMemory() {
21
+ ensureMemoryFile()
22
+ return readFileSync(MEMORY_FILE, "utf-8")
23
+ }
24
+
25
+ function writeMemory(content: string) {
26
+ ensureMemoryFile()
27
+ writeFileSync(MEMORY_FILE, content)
28
+ }
29
+
30
+ function generateId(): string {
31
+ return randomBytes(4).toString("hex")
32
+ }
33
+
34
+ const server = new McpServer(
35
+ { name: "toon-memory", version: "1.0.7" },
36
+ { capabilities: { tools: { listChanged: true } } }
37
+ )
38
+
39
+ server.registerTool(
40
+ "memory_remember",
41
+ {
42
+ title: "Save to Memory",
43
+ description: "Guarda un hecho en la memoria persistente del proyecto (decisiones, patrones, bugs, conocimiento). Se recuerda entre sesiones.",
44
+ inputSchema: {
45
+ category: z.enum(["decision", "pattern", "bug", "knowledge"]).describe("Categoría del hecho"),
46
+ key: z.string().describe("Título corto en kebab-case (ej: risk-engine-prioridad)"),
47
+ content: z.string().describe("Contenido detallado del hecho"),
48
+ file: z.string().optional().default("").describe("Archivo o línea relacionada (ej: spec.md:145)"),
49
+ tags: z.string().optional().default("").describe("Tags separados por punto y coma (ej: risk;spec)"),
50
+ },
51
+ },
52
+ async ({ category, key, content, file, tags }) => {
53
+ const data = readMemory()
54
+ const id = generateId()
55
+ const date = new Date().toISOString().split("T")[0]
56
+ const lines = data.split("\n")
57
+
58
+ let headerIdx = lines.findIndex((l) => l.startsWith("entries["))
59
+ if (headerIdx === -1) {
60
+ lines.push(`entries[0|]{id|category|key|content|file|tags|date}:`)
61
+ headerIdx = lines.length - 1
62
+ }
63
+
64
+ const match = lines[headerIdx].match(/entries\[(\d+)\|/)
65
+ const count = match ? parseInt(match[1]) : 0
66
+ const newEntry = `${id}|${category}|${key}|${content}|${file || ""}|${tags || ""}|${date}`
67
+
68
+ lines.splice(headerIdx + 1, 0, ` ${newEntry}`)
69
+ lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${count + 1}|`)
70
+
71
+ writeMemory(lines.join("\n"))
72
+ return {
73
+ content: [{ type: "text" as const, text: `🧠 Guardado: ${category}/${key} (${id})\n${content}` }],
74
+ }
75
+ }
76
+ )
77
+
78
+ server.registerTool(
79
+ "memory_recall",
80
+ {
81
+ title: "Search Memory",
82
+ description: "Busca en la memoria persistente del proyecto. Devuelve entradas relevantes. Usar ANTES de leer archivos.",
83
+ inputSchema: {
84
+ query: z.string().describe("Texto a buscar"),
85
+ category: z.string().optional().default("").describe("Filtrar por categoría (vacío = todos)"),
86
+ },
87
+ },
88
+ async ({ query, category }) => {
89
+ const data = readMemory()
90
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
91
+ const queryLower = query.toLowerCase()
92
+
93
+ const results = lines
94
+ .map((line) => {
95
+ const trimmed = line.trim()
96
+ const parts = trimmed.split("|")
97
+ if (parts.length < 7) return null
98
+ const [id, cat, key, content, file, tags, date] = parts
99
+ if (category && cat !== category) return null
100
+ const searchStr = `${id} ${cat} ${key} ${content} ${file} ${tags}`.toLowerCase()
101
+ if (!searchStr.includes(queryLower)) return null
102
+ return { id, cat, key, content, file, tags, date }
103
+ })
104
+ .filter(Boolean)
105
+
106
+ if (results.length === 0) {
107
+ return { content: [{ type: "text" as const, text: `No se encontraron resultados para "${query}"` }] }
108
+ }
109
+
110
+ const formatted = results
111
+ .map((r) => `[${r!.cat}] ${r!.key} (${r!.id})\n ${r!.content}\n File: ${r!.file} | Tags: ${r!.tags} | Date: ${r!.date}`)
112
+ .join("\n\n")
113
+
114
+ return { content: [{ type: "text" as const, text: formatted }] }
115
+ }
116
+ )
117
+
118
+ server.registerTool(
119
+ "memory_forget",
120
+ {
121
+ title: "Delete from Memory",
122
+ description: "Elimina una entrada de la memoria por su key o id.",
123
+ inputSchema: {
124
+ key: z.string().describe("Key o id de la entrada a eliminar"),
125
+ },
126
+ },
127
+ async ({ key }) => {
128
+ const data = readMemory()
129
+ const lines = data.split("\n")
130
+ const headerIdx = lines.findIndex((l) => l.startsWith("entries["))
131
+
132
+ if (headerIdx === -1) {
133
+ return { content: [{ type: "text" as const, text: "No hay entradas en memoria" }] }
134
+ }
135
+
136
+ const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0)
137
+ const filtered = entryLines.filter((l) => {
138
+ const parts = l.trim().split("|")
139
+ return parts[0] !== key && parts[2] !== key
140
+ })
141
+
142
+ const removed = entryLines.length - filtered.length
143
+ const match = lines[headerIdx].match(/entries\[(\d+)\|/)
144
+ const count = match ? parseInt(match[1]) : 0
145
+ lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${count - removed}|`)
146
+ lines.splice(headerIdx + 1, entryLines.length, ...filtered.map((l) => ` ${l.trim()}`))
147
+
148
+ writeMemory(lines.join("\n"))
149
+ return {
150
+ content: [{ type: "text" as const, text: `"${key}" eliminado. Quedan ${count - removed} entradas.` }],
151
+ }
152
+ }
153
+ )
154
+
155
+ server.registerTool(
156
+ "memory_stats",
157
+ {
158
+ title: "Memory Stats",
159
+ description: "Muestra estadísticas de la memoria del proyecto.",
160
+ inputSchema: {},
161
+ },
162
+ async () => {
163
+ const data = readMemory()
164
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
165
+ const entries = lines.map((l) => {
166
+ const parts = l.trim().split("|")
167
+ return { category: parts[1] || "unknown" }
168
+ })
169
+
170
+ const byCategory: Record<string, number> = {}
171
+ for (const e of entries) {
172
+ byCategory[e.category] = (byCategory[e.category] || 0) + 1
173
+ }
174
+
175
+ const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries"))
176
+ const stats = [
177
+ `Entradas totales: ${entries.length}`,
178
+ `Resúmenes de archivos: ${summaryLines.length}`,
179
+ "",
180
+ "Por categoría:",
181
+ ...Object.entries(byCategory).map(([k, v]) => ` ${k}: ${v}`),
182
+ "",
183
+ `Últimas 5 entradas:`,
184
+ ...lines.slice(-5).map((l) => {
185
+ const parts = l.trim().split("|")
186
+ return ` [${parts[1]}] ${parts[2]} (${parts[0]})`
187
+ }),
188
+ ]
189
+
190
+ return { content: [{ type: "text" as const, text: stats.join("\n") }] }
191
+ }
192
+ )
193
+
194
+ server.registerTool(
195
+ "memory_summary",
196
+ {
197
+ title: "File Summary",
198
+ description: "Guarda o recupera un resumen de un archivo grande para ahorrar tokens.",
199
+ inputSchema: {
200
+ action: z.enum(["get", "set"]).describe("get para leer, set para guardar"),
201
+ file: z.string().describe("Ruta del archivo"),
202
+ summary: z.string().optional().default("").describe("Resumen del archivo (solo para set)"),
203
+ },
204
+ },
205
+ async ({ action, file, summary }) => {
206
+ const data = readMemory()
207
+
208
+ if (action === "get") {
209
+ const lines = data.split("\n")
210
+ const summaryIdx = lines.findIndex((l) => l.trim().startsWith("summaries:"))
211
+ if (summaryIdx === -1) {
212
+ return { content: [{ type: "text" as const, text: `No hay resúmenes guardados para "${file}"` }] }
213
+ }
214
+
215
+ const summaryLines = lines.slice(summaryIdx + 1).filter((l) => l.includes(":"))
216
+ const match = summaryLines.find((l) => l.startsWith(` ${file}:`))
217
+ if (!match) {
218
+ return { content: [{ type: "text" as const, text: `No hay resumen para "${file}"` }] }
219
+ }
220
+
221
+ const summaryText = match.replace(` ${file}: `, "")
222
+ return { content: [{ type: "text" as const, text: summaryText }] }
223
+ }
224
+
225
+ const lines = data.split("\n")
226
+ let summaryIdx = lines.findIndex((l) => l.trim().startsWith("summaries:"))
227
+
228
+ if (summaryIdx === -1) {
229
+ lines.push("", "summaries:")
230
+ summaryIdx = lines.length - 1
231
+ }
232
+
233
+ const summaryLines = lines.slice(summaryIdx + 1).filter((l) => l.includes(":"))
234
+ const existingIdx = summaryLines.findIndex((l) => l.startsWith(` ${file}:`))
235
+
236
+ if (existingIdx !== -1) {
237
+ summaryLines[existingIdx] = ` ${file}: ${summary}`
238
+ } else {
239
+ summaryLines.push(` ${file}: ${summary}`)
240
+ }
241
+
242
+ lines.splice(summaryIdx + 1, lines.length - summaryIdx - 1, ...summaryLines)
243
+ writeMemory(lines.join("\n"))
244
+ return {
245
+ content: [{ type: "text" as const, text: `📝 Resumen guardado para ${file}` }],
246
+ }
247
+ }
248
+ )
249
+
250
+ const transport = new StdioServerTransport()
251
+ await server.connect(transport)
package/src/memory.ts ADDED
@@ -0,0 +1,237 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
3
+ import { dirname, join } from "path"
4
+ import { fileURLToPath } from "url"
5
+ import { randomBytes } from "crypto"
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url))
8
+ const MEMORY_DIR = join(__dirname, "..", "memory")
9
+ const MEMORY_FILE = join(MEMORY_DIR, "data.toon")
10
+
11
+ function ensureMemoryFile() {
12
+ if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
13
+ if (!existsSync(MEMORY_FILE)) {
14
+ writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
15
+ }
16
+ }
17
+
18
+ function readMemory() {
19
+ ensureMemoryFile()
20
+ return readFileSync(MEMORY_FILE, "utf-8")
21
+ }
22
+
23
+ function writeMemory(content: string) {
24
+ ensureMemoryFile()
25
+ writeFileSync(MEMORY_FILE, content)
26
+ }
27
+
28
+ function generateId(): string {
29
+ return randomBytes(4).toString("hex")
30
+ }
31
+
32
+ export const memoryRemember = tool({
33
+ description: "Guarda un hecho en la memoria persistente del proyecto (decisiones, patrones, bugs, conocimiento). Se recuerda entre sesiones.",
34
+ args: {
35
+ category: tool.schema.enum({
36
+ description: "Categoría del hecho",
37
+ options: ["decision", "pattern", "bug", "knowledge"],
38
+ }),
39
+ key: tool.schema.string({
40
+ description: "Título corto en kebab-case (ej: risk-engine-prioridad)",
41
+ }),
42
+ content: tool.schema.string({
43
+ description: "Contenido detallado del hecho",
44
+ }),
45
+ file: tool.schema.string({
46
+ description: "Archivo o línea relacionada (ej: spec.md:145)",
47
+ }),
48
+ tags: tool.schema.string({
49
+ description: "Tags separados por punto y coma (ej: risk;spec)",
50
+ }),
51
+ },
52
+ async execute(args) {
53
+ const data = readMemory()
54
+ const id = generateId()
55
+ const date = new Date().toISOString().split("T")[0]
56
+ const lines = data.split("\n")
57
+
58
+ let headerIdx = lines.findIndex((l) => l.startsWith("entries["))
59
+ if (headerIdx === -1) {
60
+ lines.push(`entries[0|]{id|category|key|content|file|tags|date}:`)
61
+ headerIdx = lines.length - 1
62
+ }
63
+
64
+ const match = lines[headerIdx].match(/entries\[(\d+)\|/)
65
+ const count = match ? parseInt(match[1]) : 0
66
+ const newEntry = `${id}|${args.category}|${args.key}|${args.content}|${args.file || ""}|${args.tags || ""}|${date}`
67
+
68
+ lines.splice(headerIdx + 1, 0, ` ${newEntry}`)
69
+ lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${count + 1}|`)
70
+
71
+ writeMemory(lines.join("\n"))
72
+ return `🧠 Guardado: ${args.category}/${args.key} (${id})\n${args.content}`
73
+ },
74
+ })
75
+
76
+ export const memoryRecall = tool({
77
+ description: "Busca en la memoria persistente del proyecto. Devuelve entradas relevantes por categoría, tag, o texto libre. Usar ANTES de leer archivos para evitar re-investigar.",
78
+ args: {
79
+ query: tool.schema.string({
80
+ description: "Texto a buscar (busca en key, content, tags)",
81
+ }),
82
+ category: tool.schema.string({
83
+ description: "Filtrar por categoría (decision|pattern|bug|knowledge). Vacío = todos.",
84
+ }),
85
+ },
86
+ async execute(args) {
87
+ const data = readMemory()
88
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
89
+ const queryLower = args.query.toLowerCase()
90
+
91
+ const results = lines
92
+ .map((line) => {
93
+ const trimmed = line.trim()
94
+ const parts = trimmed.split("|")
95
+ if (parts.length < 7) return null
96
+ const [id, cat, key, content, file, tags, date] = parts
97
+ if (args.category && cat !== args.category) return null
98
+ const searchStr = `${id} ${cat} ${key} ${content} ${file} ${tags}`.toLowerCase()
99
+ if (!searchStr.includes(queryLower)) return null
100
+ return { id, cat, key, content, file, tags, date }
101
+ })
102
+ .filter(Boolean)
103
+
104
+ if (results.length === 0) {
105
+ return `No se encontraron resultados para "${args.query}"`
106
+ }
107
+
108
+ return results
109
+ .map((r) => `[${r!.cat}] ${r!.key} (${r!.id})\n ${r!.content}\n File: ${r!.file} | Tags: ${r!.tags} | Date: ${r!.date}`)
110
+ .join("\n\n")
111
+ },
112
+ })
113
+
114
+ export const memoryForget = tool({
115
+ description: "Elimina una entrada de la memoria por su key o id.",
116
+ args: {
117
+ key: tool.schema.string({
118
+ description: "Key o id de la entrada a eliminar",
119
+ }),
120
+ },
121
+ async execute(args) {
122
+ const data = readMemory()
123
+ const lines = data.split("\n")
124
+ const headerIdx = lines.findIndex((l) => l.startsWith("entries["))
125
+
126
+ if (headerIdx === -1) {
127
+ return "No hay entradas en memoria"
128
+ }
129
+
130
+ const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0)
131
+ const filtered = entryLines.filter((l) => {
132
+ const parts = l.trim().split("|")
133
+ return parts[0] !== args.key && parts[2] !== args.key
134
+ })
135
+
136
+ const removed = entryLines.length - filtered.length
137
+ const match = lines[headerIdx].match(/entries\[(\d+)\|/)
138
+ const count = match ? parseInt(match[1]) : 0
139
+ lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${count - removed}|`)
140
+ lines.splice(headerIdx + 1, entryLines.length, ...filtered.map((l) => ` ${l.trim()}`))
141
+
142
+ writeMemory(lines.join("\n"))
143
+ return `"${args.key}" eliminado. Quedan ${count - removed} entradas.`
144
+ },
145
+ })
146
+
147
+ export const memoryStats = tool({
148
+ description: "Muestra estadísticas de la memoria: total entradas, por categoría, y resúmenes de archivos.",
149
+ args: {},
150
+ async execute() {
151
+ const data = readMemory()
152
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
153
+ const entries = lines.map((l) => {
154
+ const parts = l.trim().split("|")
155
+ return { category: parts[1] || "unknown" }
156
+ })
157
+
158
+ const byCategory: Record<string, number> = {}
159
+ for (const e of entries) {
160
+ byCategory[e.category] = (byCategory[e.category] || 0) + 1
161
+ }
162
+
163
+ const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries"))
164
+ const stats = [
165
+ `Entradas totales: ${entries.length}`,
166
+ `Resúmenes de archivos: ${summaryLines.length}`,
167
+ "",
168
+ "Por categoría:",
169
+ ...Object.entries(byCategory).map(([k, v]) => ` ${k}: ${v}`),
170
+ "",
171
+ `Últimas 5 entradas:`,
172
+ ...lines.slice(-5).map((l) => {
173
+ const parts = l.trim().split("|")
174
+ return ` [${parts[1]}] ${parts[2]} (${parts[0]})`
175
+ }),
176
+ ]
177
+
178
+ return stats.join("\n")
179
+ },
180
+ })
181
+
182
+ export const memorySummary = tool({
183
+ description: "Guarda o recupera un resumen de un archivo grande. El agente puede leer el resumen en vez del archivo completo para ahorrar tokens.",
184
+ args: {
185
+ action: tool.schema.enum({
186
+ description: "get para leer, set para guardar",
187
+ options: ["get", "set"],
188
+ }),
189
+ file: tool.schema.string({
190
+ description: "Ruta del archivo",
191
+ }),
192
+ summary: tool.schema.string({
193
+ description: "Resumen del archivo (solo para set)",
194
+ }),
195
+ },
196
+ async execute(args) {
197
+ const data = readMemory()
198
+
199
+ if (args.action === "get") {
200
+ const lines = data.split("\n")
201
+ const summaryIdx = lines.findIndex((l) => l.trim().startsWith("summaries:"))
202
+ if (summaryIdx === -1) {
203
+ return `No hay resúmenes guardados para "${args.file}"`
204
+ }
205
+
206
+ const summaryLines = lines.slice(summaryIdx + 1).filter((l) => l.includes(":"))
207
+ const match = summaryLines.find((l) => l.startsWith(` ${args.file}:`))
208
+ if (!match) {
209
+ return `No hay resumen para "${args.file}"`
210
+ }
211
+
212
+ const summaryText = match.replace(` ${args.file}: `, "")
213
+ return summaryText
214
+ }
215
+
216
+ const lines = data.split("\n")
217
+ let summaryIdx = lines.findIndex((l) => l.trim().startsWith("summaries:"))
218
+
219
+ if (summaryIdx === -1) {
220
+ lines.push("", "summaries:")
221
+ summaryIdx = lines.length - 1
222
+ }
223
+
224
+ const summaryLines = lines.slice(summaryIdx + 1).filter((l) => l.includes(":"))
225
+ const existingIdx = summaryLines.findIndex((l) => l.startsWith(` ${args.file}:`))
226
+
227
+ if (existingIdx !== -1) {
228
+ summaryLines[existingIdx] = ` ${args.file}: ${args.summary}`
229
+ } else {
230
+ summaryLines.push(` ${args.file}: ${args.summary}`)
231
+ }
232
+
233
+ lines.splice(summaryIdx + 1, lines.length - summaryIdx - 1, ...summaryLines)
234
+ writeMemory(lines.join("\n"))
235
+ return `📝 Resumen guardado para ${args.file}`
236
+ },
237
+ })