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 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
- - **5 MCP tools** — `memory_remember`, `memory_recall`, `memory_forget`, `memory_stats`, `memory_summary`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toon-memory",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Persistent memory system for AI coding agents using TOON format (40% fewer tokens than JSON)",
5
5
  "type": "module",
6
6
  "bin": {
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)