toon-memory 1.0.4 → 1.0.6
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/bin/setup.js +25 -62
- package/bin/toon-memory.js +0 -0
- package/mcp/server.js +24261 -17365
- package/package.json +1 -1
- package/skills/toon-memory.md +34 -0
- package/src/mcp/server.ts +227 -283
- package/src/memory.ts +189 -196
- package/README.md +0 -66
package/package.json
CHANGED
package/skills/toon-memory.md
CHANGED
|
@@ -14,6 +14,40 @@ A persistent memory system for the OpenCode AI agent. It remembers decisions, pa
|
|
|
14
14
|
| `memory_stats` | View memory state |
|
|
15
15
|
| `memory_summary` | Save/retrieve file summaries |
|
|
16
16
|
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
### Option 1: Global MCP Server (recommended)
|
|
20
|
+
|
|
21
|
+
Add to `~/.config/opencode/opencode.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcp": {
|
|
26
|
+
"toon-memory": {
|
|
27
|
+
"type": "local",
|
|
28
|
+
"command": ["npx", "-y", "toon-memory", "mcp"],
|
|
29
|
+
"enabled": true
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Option 2: Project-level MCP Server
|
|
36
|
+
|
|
37
|
+
Add to `.opencode/opencode.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcp": {
|
|
42
|
+
"toon-memory": {
|
|
43
|
+
"type": "local",
|
|
44
|
+
"command": ["npx", "-y", "toon-memory", "mcp"],
|
|
45
|
+
"enabled": true
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
17
51
|
## How to use
|
|
18
52
|
|
|
19
53
|
### At the START of every session
|
package/src/mcp/server.ts
CHANGED
|
@@ -1,307 +1,251 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
1
|
import { McpServer } from "@modelcontextprotocol/server"
|
|
4
|
-
import {
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"
|
|
3
|
+
import { z } from "zod"
|
|
5
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import * as z from "zod/v4"
|
|
10
|
-
|
|
11
|
-
// ── Memory storage ──────────────────────────────
|
|
12
|
-
|
|
13
|
-
const MEMORY_DIR = path.join(process.cwd(), ".opencode", "memory")
|
|
14
|
-
const MEMORY_FILE = path.join(MEMORY_DIR, "data.toon")
|
|
15
|
-
|
|
16
|
-
interface MemoryEntry {
|
|
17
|
-
id: string
|
|
18
|
-
category: "decision" | "pattern" | "bug" | "knowledge"
|
|
19
|
-
key: string
|
|
20
|
-
content: string
|
|
21
|
-
file: string
|
|
22
|
-
tags: string
|
|
23
|
-
date: string
|
|
24
|
-
}
|
|
5
|
+
import { dirname, join } from "path"
|
|
6
|
+
import { fileURLToPath } from "url"
|
|
7
|
+
import { randomBytes } from "crypto"
|
|
25
8
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
summaries: Record<string, string>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function defaultData(): MemoryData {
|
|
33
|
-
return { version: 1, entries: [], summaries: {} }
|
|
34
|
-
}
|
|
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")
|
|
35
12
|
|
|
36
|
-
function
|
|
13
|
+
function ensureMemoryFile() {
|
|
37
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
|
+
}
|
|
38
18
|
}
|
|
39
19
|
|
|
40
|
-
function readMemory()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const raw = readFileSync(MEMORY_FILE, "utf-8")
|
|
45
|
-
const parsed = decode(raw) as MemoryData
|
|
46
|
-
if (!parsed || typeof parsed !== "object") return defaultData()
|
|
47
|
-
if (!Array.isArray(parsed.entries)) parsed.entries = []
|
|
48
|
-
if (!parsed.summaries || typeof parsed.summaries !== "object") parsed.summaries = {}
|
|
49
|
-
return parsed
|
|
50
|
-
} catch {
|
|
51
|
-
return defaultData()
|
|
52
|
-
}
|
|
20
|
+
function readMemory() {
|
|
21
|
+
ensureMemoryFile()
|
|
22
|
+
return readFileSync(MEMORY_FILE, "utf-8")
|
|
53
23
|
}
|
|
54
24
|
|
|
55
|
-
function writeMemory(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
writeFileSync(MEMORY_FILE, toon, "utf-8")
|
|
25
|
+
function writeMemory(content: string) {
|
|
26
|
+
ensureMemoryFile()
|
|
27
|
+
writeFileSync(MEMORY_FILE, content)
|
|
59
28
|
}
|
|
60
29
|
|
|
61
|
-
function
|
|
62
|
-
return
|
|
30
|
+
function generateId(): string {
|
|
31
|
+
return randomBytes(4).toString("hex")
|
|
63
32
|
}
|
|
64
33
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
category: z
|
|
82
|
-
.enum(["decision", "pattern", "bug", "knowledge"])
|
|
83
|
-
.describe("Categoría del hecho"),
|
|
84
|
-
key: z
|
|
85
|
-
.string()
|
|
86
|
-
.describe("Título corto en kebab-case (ej: risk-engine-prioridad)"),
|
|
87
|
-
content: z.string().describe("Contenido detallado del hecho"),
|
|
88
|
-
file: z
|
|
89
|
-
.string()
|
|
90
|
-
.describe("Archivo o línea relacionada (ej: spec.md:145)")
|
|
91
|
-
.default(""),
|
|
92
|
-
tags: z
|
|
93
|
-
.string()
|
|
94
|
-
.describe("Tags separados por punto y coma (ej: risk;spec)")
|
|
95
|
-
.default(""),
|
|
96
|
-
}),
|
|
34
|
+
const server = new McpServer(
|
|
35
|
+
{ name: "toon-memory", version: "1.0.5" },
|
|
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)"),
|
|
97
50
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
{
|
|
134
|
-
title: "Search Memory",
|
|
135
|
-
description:
|
|
136
|
-
"Busca en la memoria persistente del proyecto. Devuelve entradas relevantes. Usar ANTES de leer archivos.",
|
|
137
|
-
inputSchema: z.object({
|
|
138
|
-
query: z.string().describe("Texto a buscar"),
|
|
139
|
-
category: z
|
|
140
|
-
.string()
|
|
141
|
-
.describe("Filtrar por categoría (vacío = todos)")
|
|
142
|
-
.default(""),
|
|
143
|
-
}),
|
|
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)"),
|
|
144
86
|
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
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 }
|
|
155
103
|
})
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (results.length === 0) {
|
|
159
|
-
return {
|
|
160
|
-
content: [
|
|
161
|
-
{
|
|
162
|
-
type: "text" as const,
|
|
163
|
-
text: `No encontré nada para "${query}"${category ? ` en "${category}"` : ""}.`,
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
}
|
|
167
|
-
}
|
|
104
|
+
.filter(Boolean)
|
|
168
105
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`[${e.category}] ${e.key} (${e.id})\n ${e.content}\n File: ${e.file || "N/A"} | Tags: ${e.tags || "N/A"} | Date: ${e.date}`,
|
|
173
|
-
)
|
|
174
|
-
.join("\n\n")
|
|
106
|
+
if (results.length === 0) {
|
|
107
|
+
return { content: [{ type: "text" as const, text: `No se encontraron resultados para "${query}"` }] }
|
|
108
|
+
}
|
|
175
109
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const state = readMemory()
|
|
192
|
-
const before = state.entries.length
|
|
193
|
-
state.entries = state.entries.filter(
|
|
194
|
-
(e) => e.key !== key && e.id !== key,
|
|
195
|
-
)
|
|
196
|
-
if (state.entries.length === before) {
|
|
197
|
-
return {
|
|
198
|
-
content: [
|
|
199
|
-
{
|
|
200
|
-
type: "text" as const,
|
|
201
|
-
text: `No encontré "${key}" en la memoria.`,
|
|
202
|
-
},
|
|
203
|
-
],
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
writeMemory(state)
|
|
207
|
-
return {
|
|
208
|
-
content: [
|
|
209
|
-
{
|
|
210
|
-
type: "text" as const,
|
|
211
|
-
text: `"${key}" eliminado. Quedan ${state.entries.length} entradas.`,
|
|
212
|
-
},
|
|
213
|
-
],
|
|
214
|
-
}
|
|
215
|
-
},
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
// Tool: memory_stats
|
|
219
|
-
server.registerTool(
|
|
220
|
-
"memory_stats",
|
|
221
|
-
{
|
|
222
|
-
title: "Memory Stats",
|
|
223
|
-
description: "Muestra estadísticas de la memoria del proyecto.",
|
|
224
|
-
inputSchema: z.object({}),
|
|
225
|
-
},
|
|
226
|
-
async () => {
|
|
227
|
-
const state = readMemory()
|
|
228
|
-
const byCategory: Record<string, number> = {}
|
|
229
|
-
for (const e of state.entries) {
|
|
230
|
-
byCategory[e.category] = (byCategory[e.category] || 0) + 1
|
|
231
|
-
}
|
|
232
|
-
const catStr = Object.entries(byCategory)
|
|
233
|
-
.map(([k, v]) => ` ${k}: ${v}`)
|
|
234
|
-
.join("\n")
|
|
235
|
-
const recent = state.entries
|
|
236
|
-
.slice(-5)
|
|
237
|
-
.reverse()
|
|
238
|
-
.map((e) => ` [${e.category}] ${e.key} (${e.date})`)
|
|
239
|
-
.join("\n")
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
content: [
|
|
243
|
-
{
|
|
244
|
-
type: "text" as const,
|
|
245
|
-
text: [
|
|
246
|
-
`Entradas totales: ${state.entries.length}`,
|
|
247
|
-
`Resúmenes de archivos: ${Object.keys(state.summaries).length}`,
|
|
248
|
-
"",
|
|
249
|
-
"Por categoría:",
|
|
250
|
-
catStr || " (vacío)",
|
|
251
|
-
"",
|
|
252
|
-
"Últimas 5:",
|
|
253
|
-
recent || " (ninguna)",
|
|
254
|
-
].join("\n"),
|
|
255
|
-
},
|
|
256
|
-
],
|
|
257
|
-
}
|
|
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"),
|
|
258
125
|
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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]})`
|
|
275
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)"),
|
|
276
203
|
},
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
text: `No hay resumen para "${file}".`,
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return { content: [{ type: "text" as const, text: s }] }
|
|
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}"` }] }
|
|
292
213
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
type: "text" as const,
|
|
299
|
-
text: `${file} actualizado.`,
|
|
300
|
-
},
|
|
301
|
-
],
|
|
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}"` }] }
|
|
302
219
|
}
|
|
303
|
-
},
|
|
304
|
-
)
|
|
305
220
|
|
|
306
|
-
|
|
307
|
-
}
|
|
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)
|