toon-memory 1.3.0 → 1.4.0
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/README.md +14 -1
- package/mcp/server.js +75 -0
- package/package.json +1 -1
- package/src/mcp/server.ts +91 -0
package/README.md
CHANGED
|
@@ -22,13 +22,14 @@ AI agents forget everything between sessions. toon-memory fixes this by providin
|
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
|
-
- **
|
|
25
|
+
- **6 MCP tools** — `memory_remember`, `memory_recall`, `memory_forget`, `memory_stats`, `memory_summary`, `memory_archive`
|
|
26
26
|
- **7 agents supported** — OpenCode, VS Code/Copilot, Claude Code, Cursor, Windsurf, Cline, Continue
|
|
27
27
|
- **TOON format** — 40% fewer tokens than JSON, better LLM comprehension
|
|
28
28
|
- **Per-project memory** — each project gets its own memory file
|
|
29
29
|
- **Zero config** — just install and use
|
|
30
30
|
- **Auto gitignore** — automatically adds `.opencode/memory/` to `.gitignore`
|
|
31
31
|
- **Date filtering** — search memory by date range
|
|
32
|
+
- **Auto-archive** — old entries (>30 days) moved to archive automatically
|
|
32
33
|
|
|
33
34
|
---
|
|
34
35
|
|
|
@@ -89,6 +90,7 @@ memory_remember # Save important decisions
|
|
|
89
90
|
| `memory_forget` | Remove an entry by key or id |
|
|
90
91
|
| `memory_stats` | View memory state |
|
|
91
92
|
| `memory_summary` | Save/retrieve file summaries |
|
|
93
|
+
| `memory_archive` | Archive old entries (>30 days) |
|
|
92
94
|
|
|
93
95
|
### Date Filtering
|
|
94
96
|
|
|
@@ -103,6 +105,17 @@ memory_recall({
|
|
|
103
105
|
})
|
|
104
106
|
```
|
|
105
107
|
|
|
108
|
+
### Auto-Archive
|
|
109
|
+
|
|
110
|
+
Entries older than 30 days are automatically archived to keep memory clean:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Manually trigger archiving
|
|
114
|
+
memory_archive()
|
|
115
|
+
// 📦 Archivadas 5 entradas antiguas
|
|
116
|
+
// 📋 Quedan 42 entradas activas
|
|
117
|
+
```
|
|
118
|
+
|
|
106
119
|
---
|
|
107
120
|
|
|
108
121
|
## How It Works
|
package/mcp/server.js
CHANGED
|
@@ -27646,6 +27646,8 @@ import { randomBytes } from "crypto";
|
|
|
27646
27646
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27647
27647
|
var MEMORY_DIR = join(process.cwd(), ".opencode", "memory");
|
|
27648
27648
|
var MEMORY_FILE = join(MEMORY_DIR, "data.toon");
|
|
27649
|
+
var ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon");
|
|
27650
|
+
var ARCHIVE_DAYS = 30;
|
|
27649
27651
|
function ensureMemoryFile() {
|
|
27650
27652
|
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
|
|
27651
27653
|
if (!existsSync(MEMORY_FILE)) {
|
|
@@ -27663,6 +27665,58 @@ function writeMemory(content) {
|
|
|
27663
27665
|
function generateId() {
|
|
27664
27666
|
return randomBytes(4).toString("hex");
|
|
27665
27667
|
}
|
|
27668
|
+
function archiveOldEntries() {
|
|
27669
|
+
const data = readMemory();
|
|
27670
|
+
const lines = data.split("\n");
|
|
27671
|
+
const headerIdx = lines.findIndex((l) => l.startsWith("entries["));
|
|
27672
|
+
if (headerIdx === -1) return { archived: 0, kept: 0 };
|
|
27673
|
+
const today = /* @__PURE__ */ new Date();
|
|
27674
|
+
const cutoff = new Date(today);
|
|
27675
|
+
cutoff.setDate(cutoff.getDate() - ARCHIVE_DAYS);
|
|
27676
|
+
const cutoffStr = cutoff.toISOString().split("T")[0];
|
|
27677
|
+
const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0);
|
|
27678
|
+
const toArchive = [];
|
|
27679
|
+
const toKeep = [];
|
|
27680
|
+
for (const line of entryLines) {
|
|
27681
|
+
const parts = line.trim().split("|");
|
|
27682
|
+
if (parts.length >= 7) {
|
|
27683
|
+
const date5 = parts[6];
|
|
27684
|
+
if (date5 < cutoffStr) {
|
|
27685
|
+
toArchive.push(line.trim());
|
|
27686
|
+
} else {
|
|
27687
|
+
toKeep.push(line.trim());
|
|
27688
|
+
}
|
|
27689
|
+
} else {
|
|
27690
|
+
toKeep.push(line.trim());
|
|
27691
|
+
}
|
|
27692
|
+
}
|
|
27693
|
+
if (toArchive.length === 0) return { archived: 0, kept: toKeep.length };
|
|
27694
|
+
let archiveContent = "";
|
|
27695
|
+
if (existsSync(ARCHIVE_FILE)) {
|
|
27696
|
+
archiveContent = readFileSync(ARCHIVE_FILE, "utf-8");
|
|
27697
|
+
} else {
|
|
27698
|
+
archiveContent = `version: 1
|
|
27699
|
+
archived:
|
|
27700
|
+
`;
|
|
27701
|
+
}
|
|
27702
|
+
const archiveLines = archiveContent.split("\n");
|
|
27703
|
+
let archiveHeaderIdx = archiveLines.findIndex((l) => l.startsWith("archived["));
|
|
27704
|
+
if (archiveHeaderIdx === -1) {
|
|
27705
|
+
archiveLines.push(`archived[0|]{id|category|key|content|file|tags|date}:`);
|
|
27706
|
+
archiveHeaderIdx = archiveLines.length - 1;
|
|
27707
|
+
}
|
|
27708
|
+
const archiveMatch = archiveLines[archiveHeaderIdx].match(/archived\[(\d+)\|/);
|
|
27709
|
+
const archiveCount = archiveMatch ? parseInt(archiveMatch[1]) : 0;
|
|
27710
|
+
for (const entry of toArchive) {
|
|
27711
|
+
archiveLines.splice(archiveHeaderIdx + 1, 0, ` ${entry}`);
|
|
27712
|
+
}
|
|
27713
|
+
archiveLines[archiveHeaderIdx] = archiveLines[archiveHeaderIdx].replace(/archived\[\d+\|/, `[${archiveCount + toArchive.length}|`);
|
|
27714
|
+
writeFileSync(ARCHIVE_FILE, archiveLines.join("\n"));
|
|
27715
|
+
lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${toKeep.length}|`);
|
|
27716
|
+
lines.splice(headerIdx + 1, entryLines.length, ...toKeep.map((l) => ` ${l}`));
|
|
27717
|
+
writeMemory(lines.join("\n"));
|
|
27718
|
+
return { archived: toArchive.length, kept: toKeep.length };
|
|
27719
|
+
}
|
|
27666
27720
|
var server = new McpServer(
|
|
27667
27721
|
{ name: "toon-memory", version: "1.1.0" },
|
|
27668
27722
|
{ capabilities: { tools: { listChanged: true } } }
|
|
@@ -27853,6 +27907,27 @@ server.registerTool(
|
|
|
27853
27907
|
};
|
|
27854
27908
|
}
|
|
27855
27909
|
);
|
|
27910
|
+
server.registerTool(
|
|
27911
|
+
"memory_archive",
|
|
27912
|
+
{
|
|
27913
|
+
title: "Archive Old Entries",
|
|
27914
|
+
description: "Mover entradas antiguas (>30 d\xEDas) a archive.toon para mantener la memoria limpia.",
|
|
27915
|
+
inputSchema: {}
|
|
27916
|
+
},
|
|
27917
|
+
async () => {
|
|
27918
|
+
const result = archiveOldEntries();
|
|
27919
|
+
if (result.archived === 0) {
|
|
27920
|
+
return { content: [{ type: "text", text: "No hay entradas antiguas para archivar" }] };
|
|
27921
|
+
}
|
|
27922
|
+
return {
|
|
27923
|
+
content: [{
|
|
27924
|
+
type: "text",
|
|
27925
|
+
text: `\u{1F4E6} Archivadas ${result.archived} entradas antiguas
|
|
27926
|
+
\u{1F4CB} Quedan ${result.kept} entradas activas`
|
|
27927
|
+
}]
|
|
27928
|
+
};
|
|
27929
|
+
}
|
|
27930
|
+
);
|
|
27856
27931
|
var transport = new StdioServerTransport();
|
|
27857
27932
|
await server.connect(transport);
|
|
27858
27933
|
/*! Bundled license information:
|
package/package.json
CHANGED
package/src/mcp/server.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { randomBytes } from "crypto"
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
const MEMORY_DIR = join(process.cwd(), ".opencode", "memory")
|
|
11
11
|
const MEMORY_FILE = join(MEMORY_DIR, "data.toon")
|
|
12
|
+
const ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon")
|
|
13
|
+
const MAX_ENTRIES = 100
|
|
14
|
+
const ARCHIVE_DAYS = 30
|
|
12
15
|
|
|
13
16
|
function ensureMemoryFile() {
|
|
14
17
|
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
|
@@ -31,6 +34,71 @@ function generateId(): string {
|
|
|
31
34
|
return randomBytes(4).toString("hex")
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
function archiveOldEntries(): { archived: number; kept: number } {
|
|
38
|
+
const data = readMemory()
|
|
39
|
+
const lines = data.split("\n")
|
|
40
|
+
const headerIdx = lines.findIndex((l) => l.startsWith("entries["))
|
|
41
|
+
|
|
42
|
+
if (headerIdx === -1) return { archived: 0, kept: 0 }
|
|
43
|
+
|
|
44
|
+
const today = new Date()
|
|
45
|
+
const cutoff = new Date(today)
|
|
46
|
+
cutoff.setDate(cutoff.getDate() - ARCHIVE_DAYS)
|
|
47
|
+
const cutoffStr = cutoff.toISOString().split("T")[0]
|
|
48
|
+
|
|
49
|
+
const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0)
|
|
50
|
+
const toArchive: string[] = []
|
|
51
|
+
const toKeep: string[] = []
|
|
52
|
+
|
|
53
|
+
for (const line of entryLines) {
|
|
54
|
+
const parts = line.trim().split("|")
|
|
55
|
+
if (parts.length >= 7) {
|
|
56
|
+
const date = parts[6]
|
|
57
|
+
if (date < cutoffStr) {
|
|
58
|
+
toArchive.push(line.trim())
|
|
59
|
+
} else {
|
|
60
|
+
toKeep.push(line.trim())
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
toKeep.push(line.trim())
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (toArchive.length === 0) return { archived: 0, kept: toKeep.length }
|
|
68
|
+
|
|
69
|
+
// Write archived entries
|
|
70
|
+
let archiveContent = ""
|
|
71
|
+
if (existsSync(ARCHIVE_FILE)) {
|
|
72
|
+
archiveContent = readFileSync(ARCHIVE_FILE, "utf-8")
|
|
73
|
+
} else {
|
|
74
|
+
archiveContent = `version: 1\narchived:\n`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const archiveLines = archiveContent.split("\n")
|
|
78
|
+
let archiveHeaderIdx = archiveLines.findIndex((l) => l.startsWith("archived["))
|
|
79
|
+
if (archiveHeaderIdx === -1) {
|
|
80
|
+
archiveLines.push(`archived[0|]{id|category|key|content|file|tags|date}:`)
|
|
81
|
+
archiveHeaderIdx = archiveLines.length - 1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const archiveMatch = archiveLines[archiveHeaderIdx].match(/archived\[(\d+)\|/)
|
|
85
|
+
const archiveCount = archiveMatch ? parseInt(archiveMatch[1]) : 0
|
|
86
|
+
|
|
87
|
+
for (const entry of toArchive) {
|
|
88
|
+
archiveLines.splice(archiveHeaderIdx + 1, 0, ` ${entry}`)
|
|
89
|
+
}
|
|
90
|
+
archiveLines[archiveHeaderIdx] = archiveLines[archiveHeaderIdx].replace(/archived\[\d+\|/, `[${archiveCount + toArchive.length}|`)
|
|
91
|
+
|
|
92
|
+
writeFileSync(ARCHIVE_FILE, archiveLines.join("\n"))
|
|
93
|
+
|
|
94
|
+
// Update main file
|
|
95
|
+
lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${toKeep.length}|`)
|
|
96
|
+
lines.splice(headerIdx + 1, entryLines.length, ...toKeep.map((l) => ` ${l}`))
|
|
97
|
+
writeMemory(lines.join("\n"))
|
|
98
|
+
|
|
99
|
+
return { archived: toArchive.length, kept: toKeep.length }
|
|
100
|
+
}
|
|
101
|
+
|
|
34
102
|
const server = new McpServer(
|
|
35
103
|
{ name: "toon-memory", version: "1.1.0" },
|
|
36
104
|
{ capabilities: { tools: { listChanged: true } } }
|
|
@@ -251,5 +319,28 @@ server.registerTool(
|
|
|
251
319
|
}
|
|
252
320
|
)
|
|
253
321
|
|
|
322
|
+
server.registerTool(
|
|
323
|
+
"memory_archive",
|
|
324
|
+
{
|
|
325
|
+
title: "Archive Old Entries",
|
|
326
|
+
description: "Mover entradas antiguas (>30 días) a archive.toon para mantener la memoria limpia.",
|
|
327
|
+
inputSchema: {},
|
|
328
|
+
},
|
|
329
|
+
async () => {
|
|
330
|
+
const result = archiveOldEntries()
|
|
331
|
+
|
|
332
|
+
if (result.archived === 0) {
|
|
333
|
+
return { content: [{ type: "text" as const, text: "No hay entradas antiguas para archivar" }] }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
content: [{
|
|
338
|
+
type: "text" as const,
|
|
339
|
+
text: `📦 Archivadas ${result.archived} entradas antiguas\n📋 Quedan ${result.kept} entradas activas`
|
|
340
|
+
}],
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
|
|
254
345
|
const transport = new StdioServerTransport()
|
|
255
346
|
await server.connect(transport)
|