toon-memory 1.0.3 → 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.
@@ -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
- })