toon-memory 1.0.4 → 1.0.5
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/package.json +1 -1
- package/README.md +0 -66
- package/bin/toon-memory.js +0 -16
- package/mcp/server.js +0 -20966
- package/skills/toon-memory.md +0 -68
- package/src/mcp/server.ts +0 -307
- package/src/memory.ts +0 -244
package/skills/toon-memory.md
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
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
|
-
## How to use
|
|
18
|
-
|
|
19
|
-
### At the START of every session
|
|
20
|
-
|
|
21
|
-
1. Run `memory_stats` to see what's in memory.
|
|
22
|
-
2. If the user asks something that might be in memory → `memory_recall(query)` BEFORE reading files.
|
|
23
|
-
|
|
24
|
-
### When making decisions
|
|
25
|
-
|
|
26
|
-
- Before implementing a non-trivial change → `memory_remember(category='decision')`
|
|
27
|
-
- When you resolve a complex bug → `memory_remember(category='bug')`
|
|
28
|
-
- When you observe a code pattern → `memory_remember(category='pattern')`
|
|
29
|
-
|
|
30
|
-
### When exploring the project
|
|
31
|
-
|
|
32
|
-
1. First: `memory_recall(query="what you need to know")`
|
|
33
|
-
2. If no result: use CodeGraph or grep
|
|
34
|
-
3. Only if both fail: read files directly
|
|
35
|
-
|
|
36
|
-
### At the END of every session
|
|
37
|
-
|
|
38
|
-
1. If you made design decisions → `memory_remember(category='decision')`
|
|
39
|
-
2. If you resolved a bug → `memory_remember(category='bug')`
|
|
40
|
-
3. If you observed a pattern → `memory_remember(category='pattern')`
|
|
41
|
-
|
|
42
|
-
## Categories
|
|
43
|
-
|
|
44
|
-
| Category | When | Example |
|
|
45
|
-
|----------|------|---------|
|
|
46
|
-
| `decision` | Non-trivial design decision | "Use pandas instead of numpy for time series" |
|
|
47
|
-
| `pattern` | Observed code pattern | "This project uses Pydantic v2 for configs" |
|
|
48
|
-
| `bug` | Complex bug resolved | "Redis pool exhaustion, missing max_connections" |
|
|
49
|
-
| `knowledge` | General project knowledge | "Broker uses RESP protocol, not Redis-specific" |
|
|
50
|
-
|
|
51
|
-
## File format
|
|
52
|
-
|
|
53
|
-
Data is stored in `.opencode/memory/data.toon` using TOON (Token-Oriented Object Notation):
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
version: 1
|
|
57
|
-
entries[2|]{id|category|key|content|file|tags|date}:
|
|
58
|
-
a1b2c3d4|decision|risk-priority|Risk Engine has priority|spec.md:145|risk;spec|2026-07-10
|
|
59
|
-
e5f6g7h8|pattern|pandas-over-numpy|Analytics uses pandas|indicators.py|python;analytics|2026-07-10
|
|
60
|
-
summaries:
|
|
61
|
-
path/to/big-file.py: Brief summary of what this file does
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Anti-patterns
|
|
65
|
-
|
|
66
|
-
- Don't store trivia in memory (only important facts)
|
|
67
|
-
- Don't forget to save a decision before implementing
|
|
68
|
-
- Don't re-read files that already have a summary in `memory_summary`
|
package/src/mcp/server.ts
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { McpServer } from "@modelcontextprotocol/server"
|
|
4
|
-
import { serveStdio } from "@modelcontextprotocol/server/stdio"
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
|
|
6
|
-
import * as path from "path"
|
|
7
|
-
import { encode, decode } from "@toon-format/toon"
|
|
8
|
-
import { randomUUID } from "crypto"
|
|
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
|
-
}
|
|
25
|
-
|
|
26
|
-
interface MemoryData {
|
|
27
|
-
version: number
|
|
28
|
-
entries: MemoryEntry[]
|
|
29
|
-
summaries: Record<string, string>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function defaultData(): MemoryData {
|
|
33
|
-
return { version: 1, entries: [], summaries: {} }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function ensureDir() {
|
|
37
|
-
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function readMemory(): MemoryData {
|
|
41
|
-
ensureDir()
|
|
42
|
-
if (!existsSync(MEMORY_FILE)) return defaultData()
|
|
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
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function writeMemory(data: MemoryData): void {
|
|
56
|
-
ensureDir()
|
|
57
|
-
const toon = encode(data, { delimiter: "|", indent: 2 })
|
|
58
|
-
writeFileSync(MEMORY_FILE, toon, "utf-8")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function genId(): string {
|
|
62
|
-
return randomUUID().slice(0, 8)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── MCP Server ──────────────────────────────────
|
|
66
|
-
|
|
67
|
-
serveStdio(() => {
|
|
68
|
-
const server = new McpServer({
|
|
69
|
-
name: "toon-memory",
|
|
70
|
-
version: "1.0.1",
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Tool: memory_remember
|
|
74
|
-
server.registerTool(
|
|
75
|
-
"memory_remember",
|
|
76
|
-
{
|
|
77
|
-
title: "Save to Memory",
|
|
78
|
-
description:
|
|
79
|
-
"Guarda un hecho en la memoria persistente del proyecto (decisiones, patrones, bugs, conocimiento). Se recuerda entre sesiones.",
|
|
80
|
-
inputSchema: z.object({
|
|
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
|
-
}),
|
|
97
|
-
},
|
|
98
|
-
async ({ category, key, content, file, tags }) => {
|
|
99
|
-
const state = readMemory()
|
|
100
|
-
const existing = state.entries.find(
|
|
101
|
-
(e) => e.category === category && e.key === key,
|
|
102
|
-
)
|
|
103
|
-
const entry: MemoryEntry = {
|
|
104
|
-
id: existing?.id || genId(),
|
|
105
|
-
category,
|
|
106
|
-
key,
|
|
107
|
-
content,
|
|
108
|
-
file,
|
|
109
|
-
tags,
|
|
110
|
-
date: new Date().toISOString().slice(0, 10),
|
|
111
|
-
}
|
|
112
|
-
if (existing) {
|
|
113
|
-
const idx = state.entries.indexOf(existing)
|
|
114
|
-
state.entries[idx] = entry
|
|
115
|
-
} else {
|
|
116
|
-
state.entries.push(entry)
|
|
117
|
-
}
|
|
118
|
-
writeMemory(state)
|
|
119
|
-
return {
|
|
120
|
-
content: [
|
|
121
|
-
{
|
|
122
|
-
type: "text" as const,
|
|
123
|
-
text: `${existing ? "🧠 Actualizado" : "🧠 Guardado"}: ${category}/${key} (${entry.id})\n${content}`,
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
// Tool: memory_recall
|
|
131
|
-
server.registerTool(
|
|
132
|
-
"memory_recall",
|
|
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
|
-
}),
|
|
144
|
-
},
|
|
145
|
-
async ({ query, category }) => {
|
|
146
|
-
const state = readMemory()
|
|
147
|
-
const q = query.toLowerCase()
|
|
148
|
-
let results = state.entries.filter((e) => {
|
|
149
|
-
const matchText =
|
|
150
|
-
e.key.toLowerCase().includes(q) ||
|
|
151
|
-
e.content.toLowerCase().includes(q) ||
|
|
152
|
-
e.tags.toLowerCase().includes(q)
|
|
153
|
-
const matchCat = category ? e.category === category : true
|
|
154
|
-
return matchText && matchCat
|
|
155
|
-
})
|
|
156
|
-
results = results.slice(0, 10)
|
|
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
|
-
}
|
|
168
|
-
|
|
169
|
-
const output = results
|
|
170
|
-
.map(
|
|
171
|
-
(e) =>
|
|
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")
|
|
175
|
-
|
|
176
|
-
return { content: [{ type: "text" as const, text: output }] }
|
|
177
|
-
},
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
// Tool: memory_forget
|
|
181
|
-
server.registerTool(
|
|
182
|
-
"memory_forget",
|
|
183
|
-
{
|
|
184
|
-
title: "Delete from Memory",
|
|
185
|
-
description: "Elimina una entrada de la memoria por su key o id.",
|
|
186
|
-
inputSchema: z.object({
|
|
187
|
-
key: z.string().describe("Key o id de la entrada a eliminar"),
|
|
188
|
-
}),
|
|
189
|
-
},
|
|
190
|
-
async ({ key }) => {
|
|
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
|
-
}
|
|
258
|
-
},
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
// Tool: memory_summary
|
|
262
|
-
server.registerTool(
|
|
263
|
-
"memory_summary",
|
|
264
|
-
{
|
|
265
|
-
title: "File Summary",
|
|
266
|
-
description:
|
|
267
|
-
"Guarda o recupera un resumen de un archivo grande para ahorrar tokens.",
|
|
268
|
-
inputSchema: z.object({
|
|
269
|
-
action: z.enum(["get", "set"]).describe("get para leer, set para guardar"),
|
|
270
|
-
file: z.string().describe("Ruta del archivo"),
|
|
271
|
-
summary: z
|
|
272
|
-
.string()
|
|
273
|
-
.describe("Resumen del archivo (solo para set)")
|
|
274
|
-
.default(""),
|
|
275
|
-
}),
|
|
276
|
-
},
|
|
277
|
-
async ({ action, file, summary }) => {
|
|
278
|
-
const state = readMemory()
|
|
279
|
-
if (action === "get") {
|
|
280
|
-
const s = state.summaries[file]
|
|
281
|
-
if (!s) {
|
|
282
|
-
return {
|
|
283
|
-
content: [
|
|
284
|
-
{
|
|
285
|
-
type: "text" as const,
|
|
286
|
-
text: `No hay resumen para "${file}".`,
|
|
287
|
-
},
|
|
288
|
-
],
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return { content: [{ type: "text" as const, text: s }] }
|
|
292
|
-
}
|
|
293
|
-
state.summaries[file] = summary
|
|
294
|
-
writeMemory(state)
|
|
295
|
-
return {
|
|
296
|
-
content: [
|
|
297
|
-
{
|
|
298
|
-
type: "text" as const,
|
|
299
|
-
text: `${file} actualizado.`,
|
|
300
|
-
},
|
|
301
|
-
],
|
|
302
|
-
}
|
|
303
|
-
},
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
return server
|
|
307
|
-
})
|
package/src/memory.ts
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
|
|
3
|
-
import * as path from "path"
|
|
4
|
-
import { encode, decode } from "@toon-format/toon"
|
|
5
|
-
import { randomUUID } from "crypto"
|
|
6
|
-
|
|
7
|
-
const MEMORY_DIR = path.join(process.cwd(), ".opencode", "memory")
|
|
8
|
-
const MEMORY_FILE = path.join(MEMORY_DIR, "data.toon")
|
|
9
|
-
|
|
10
|
-
interface MemoryEntry {
|
|
11
|
-
id: string
|
|
12
|
-
category: "decision" | "pattern" | "bug" | "knowledge"
|
|
13
|
-
key: string
|
|
14
|
-
content: string
|
|
15
|
-
file: string
|
|
16
|
-
tags: string
|
|
17
|
-
date: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface MemoryData {
|
|
21
|
-
version: number
|
|
22
|
-
entries: MemoryEntry[]
|
|
23
|
-
summaries: Record<string, string>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function defaultData(): MemoryData {
|
|
27
|
-
return { version: 1, entries: [], summaries: {} }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function ensureDir() {
|
|
31
|
-
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function readMemory(): MemoryData {
|
|
35
|
-
ensureDir()
|
|
36
|
-
if (!existsSync(MEMORY_FILE)) return defaultData()
|
|
37
|
-
try {
|
|
38
|
-
const raw = readFileSync(MEMORY_FILE, "utf-8")
|
|
39
|
-
const parsed = decode(raw) as MemoryData
|
|
40
|
-
if (!parsed || typeof parsed !== "object") return defaultData()
|
|
41
|
-
if (!Array.isArray(parsed.entries)) parsed.entries = []
|
|
42
|
-
if (!parsed.summaries || typeof parsed.summaries !== "object") parsed.summaries = {}
|
|
43
|
-
return parsed
|
|
44
|
-
} catch {
|
|
45
|
-
return defaultData()
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function writeMemory(data: MemoryData): void {
|
|
50
|
-
ensureDir()
|
|
51
|
-
const toon = encode(data, { delimiter: "|", indent: 2 })
|
|
52
|
-
writeFileSync(MEMORY_FILE, toon, "utf-8")
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function genId(): string {
|
|
56
|
-
return randomUUID().slice(0, 8)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const memory_remember = tool({
|
|
60
|
-
description:
|
|
61
|
-
"Guarda un hecho en la memoria persistente del proyecto (decisiones, patrones, bugs, conocimiento). Se recuerda entre sesiones. Usar cuando el agente tome una decisión importante, resuelva un bug, o observe un patrón.",
|
|
62
|
-
args: {
|
|
63
|
-
category: tool.schema
|
|
64
|
-
.enum(["decision", "pattern", "bug", "knowledge"])
|
|
65
|
-
.describe("Categoría: decision (diseño), pattern (código), bug (resolución), knowledge (general)"),
|
|
66
|
-
key: tool.schema
|
|
67
|
-
.string()
|
|
68
|
-
.describe("Título corto en kebab-case (ej: risk-engine-prioridad)"),
|
|
69
|
-
content: tool.schema
|
|
70
|
-
.string()
|
|
71
|
-
.describe("Contenido detallado del hecho"),
|
|
72
|
-
file: tool.schema
|
|
73
|
-
.string()
|
|
74
|
-
.describe("Archivo o línea relacionada (ej: spec.md:145)")
|
|
75
|
-
.default(""),
|
|
76
|
-
tags: tool.schema
|
|
77
|
-
.string()
|
|
78
|
-
.describe("Tags separados por punto y coma (ej: risk;spec)")
|
|
79
|
-
.default(""),
|
|
80
|
-
},
|
|
81
|
-
async execute({ category, key, content, file, tags }) {
|
|
82
|
-
const state = readMemory()
|
|
83
|
-
const existing = state.entries.find(
|
|
84
|
-
(e) => e.category === category && e.key === key,
|
|
85
|
-
)
|
|
86
|
-
const entry: MemoryEntry = {
|
|
87
|
-
id: existing?.id || genId(),
|
|
88
|
-
category,
|
|
89
|
-
key,
|
|
90
|
-
content,
|
|
91
|
-
file,
|
|
92
|
-
tags,
|
|
93
|
-
date: new Date().toISOString().slice(0, 10),
|
|
94
|
-
}
|
|
95
|
-
if (existing) {
|
|
96
|
-
const idx = state.entries.indexOf(existing)
|
|
97
|
-
state.entries[idx] = entry
|
|
98
|
-
} else {
|
|
99
|
-
state.entries.push(entry)
|
|
100
|
-
}
|
|
101
|
-
writeMemory(state)
|
|
102
|
-
return {
|
|
103
|
-
title: existing ? "🧠 Actualizado" : "🧠 Guardado",
|
|
104
|
-
output: `${category}/${key} (${entry.id})\n${content}`,
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
export const memory_recall = tool({
|
|
110
|
-
description:
|
|
111
|
-
"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.",
|
|
112
|
-
args: {
|
|
113
|
-
query: tool.schema
|
|
114
|
-
.string()
|
|
115
|
-
.describe("Texto a buscar (busca en key, content, tags)"),
|
|
116
|
-
category: tool.schema
|
|
117
|
-
.string()
|
|
118
|
-
.describe("Filtrar por categoría (decision|pattern|bug|knowledge). Vacío = todos.")
|
|
119
|
-
.default(""),
|
|
120
|
-
},
|
|
121
|
-
async execute({ query, category }) {
|
|
122
|
-
const state = readMemory()
|
|
123
|
-
const q = query.toLowerCase()
|
|
124
|
-
let results = state.entries.filter((e) => {
|
|
125
|
-
const matchText =
|
|
126
|
-
e.key.toLowerCase().includes(q) ||
|
|
127
|
-
e.content.toLowerCase().includes(q) ||
|
|
128
|
-
e.tags.toLowerCase().includes(q)
|
|
129
|
-
const matchCat = category ? e.category === category : true
|
|
130
|
-
return matchText && matchCat
|
|
131
|
-
})
|
|
132
|
-
results = results.slice(0, 10)
|
|
133
|
-
if (results.length === 0) {
|
|
134
|
-
return {
|
|
135
|
-
title: "🔍 Sin resultados",
|
|
136
|
-
output: `No encontré nada para "${query}"${category ? ` en categoría "${category}"` : ""}.`,
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const output = results
|
|
140
|
-
.map(
|
|
141
|
-
(e) =>
|
|
142
|
-
`[${e.category}] ${e.key} (${e.id})\n ${e.content}\n File: ${e.file || "N/A"} | Tags: ${e.tags || "N/A"} | Date: ${e.date}`,
|
|
143
|
-
)
|
|
144
|
-
.join("\n\n")
|
|
145
|
-
return { title: `🔍 ${results.length} resultados`, output }
|
|
146
|
-
},
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
export const memory_forget = tool({
|
|
150
|
-
description: "Elimina una entrada de la memoria por su key o id.",
|
|
151
|
-
args: {
|
|
152
|
-
key: tool.schema
|
|
153
|
-
.string()
|
|
154
|
-
.describe("Key o id de la entrada a eliminar"),
|
|
155
|
-
},
|
|
156
|
-
async execute({ key }) {
|
|
157
|
-
const state = readMemory()
|
|
158
|
-
const before = state.entries.length
|
|
159
|
-
state.entries = state.entries.filter(
|
|
160
|
-
(e) => e.key !== key && e.id !== key,
|
|
161
|
-
)
|
|
162
|
-
if (state.entries.length === before) {
|
|
163
|
-
return {
|
|
164
|
-
title: "⚠️ No encontrado",
|
|
165
|
-
output: `No encontré "${key}" en la memoria.`,
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
writeMemory(state)
|
|
169
|
-
return {
|
|
170
|
-
title: "🗑️ Eliminado",
|
|
171
|
-
output: `"${key}" eliminado. Quedan ${state.entries.length} entradas.`,
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
export const memory_stats = tool({
|
|
177
|
-
description:
|
|
178
|
-
"Muestra estadísticas de la memoria: total entradas, por categoría, y resúmenes de archivos.",
|
|
179
|
-
args: {},
|
|
180
|
-
async execute() {
|
|
181
|
-
const state = readMemory()
|
|
182
|
-
const byCategory: Record<string, number> = {}
|
|
183
|
-
for (const e of state.entries) {
|
|
184
|
-
byCategory[e.category] = (byCategory[e.category] || 0) + 1
|
|
185
|
-
}
|
|
186
|
-
const catStr = Object.entries(byCategory)
|
|
187
|
-
.map(([k, v]) => ` ${k}: ${v}`)
|
|
188
|
-
.join("\n")
|
|
189
|
-
const recent = state.entries
|
|
190
|
-
.slice(-5)
|
|
191
|
-
.reverse()
|
|
192
|
-
.map((e) => ` [${e.category}] ${e.key} (${e.date})`)
|
|
193
|
-
.join("\n")
|
|
194
|
-
return {
|
|
195
|
-
title: "📊 Memoria del proyecto",
|
|
196
|
-
output: [
|
|
197
|
-
`Entradas totales: ${state.entries.length}`,
|
|
198
|
-
`Resúmenes de archivos: ${Object.keys(state.summaries).length}`,
|
|
199
|
-
"",
|
|
200
|
-
"Por categoría:",
|
|
201
|
-
catStr || " (vacío)",
|
|
202
|
-
"",
|
|
203
|
-
"Últimas 5 entradas:",
|
|
204
|
-
recent || " (ninguna)",
|
|
205
|
-
].join("\n"),
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
export const memory_summary = tool({
|
|
211
|
-
description:
|
|
212
|
-
"Guarda o recupera un resumen de un archivo grande. El agente puede leer el resumen en vez del archivo completo para ahorrar tokens.",
|
|
213
|
-
args: {
|
|
214
|
-
action: tool.schema
|
|
215
|
-
.enum(["get", "set"])
|
|
216
|
-
.describe("get para leer, set para guardar"),
|
|
217
|
-
file: tool.schema
|
|
218
|
-
.string()
|
|
219
|
-
.describe("Ruta del archivo"),
|
|
220
|
-
summary: tool.schema
|
|
221
|
-
.string()
|
|
222
|
-
.describe("Resumen del archivo (solo para set)")
|
|
223
|
-
.default(""),
|
|
224
|
-
},
|
|
225
|
-
async execute({ action, file, summary }) {
|
|
226
|
-
const state = readMemory()
|
|
227
|
-
if (action === "get") {
|
|
228
|
-
const s = state.summaries[file]
|
|
229
|
-
if (!s) {
|
|
230
|
-
return {
|
|
231
|
-
title: "📄 Sin resumen",
|
|
232
|
-
output: `No hay resumen para "${file}". Usar memory_summary action=set para guardar uno.`,
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return { title: `📄 ${file}`, output: s }
|
|
236
|
-
}
|
|
237
|
-
state.summaries[file] = summary
|
|
238
|
-
writeMemory(state)
|
|
239
|
-
return {
|
|
240
|
-
title: "📄 Resumen guardado",
|
|
241
|
-
output: `${file} actualizado.`,
|
|
242
|
-
}
|
|
243
|
-
},
|
|
244
|
-
})
|