toon-memory 1.2.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,11 +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
+ - **Auto gitignore** — automatically adds `.opencode/memory/` to `.gitignore`
31
+ - **Date filtering** — search memory by date range
32
+ - **Auto-archive** — old entries (>30 days) moved to archive automatically
30
33
 
31
34
  ---
32
35
 
@@ -87,6 +90,31 @@ memory_remember # Save important decisions
87
90
  | `memory_forget` | Remove an entry by key or id |
88
91
  | `memory_stats` | View memory state |
89
92
  | `memory_summary` | Save/retrieve file summaries |
93
+ | `memory_archive` | Archive old entries (>30 days) |
94
+
95
+ ### Date Filtering
96
+
97
+ Search memory by date range:
98
+
99
+ ```typescript
100
+ // Search with date filter
101
+ memory_recall({
102
+ query: "redis",
103
+ from_date: "2026-07-01",
104
+ to_date: "2026-07-31"
105
+ })
106
+ ```
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
+ ```
90
118
 
91
119
  ---
92
120
 
@@ -114,12 +142,55 @@ summaries:
114
142
  ## CLI Commands
115
143
 
116
144
  ```bash
117
- npx toon-memory # Interactive installer
118
- npx toon-memory init # Quick setup (no prompts)
119
- npx toon-memory mcp # Run MCP server directly
120
- npx toon-memory status # Check installation status
121
- npx toon-memory upgrade # Update to latest version
122
- npx toon-memory uninstall # Remove from all agents
145
+ npx toon-memory # Interactive installer
146
+ npx toon-memory init # Quick setup (no prompts)
147
+ npx toon-memory mcp # Run MCP server directly
148
+ npx toon-memory status # Check installation status
149
+ npx toon-memory stats # View memory statistics
150
+ npx toon-memory export # Export memory to JSON
151
+ npx toon-memory import <file> # Import memory from JSON
152
+ npx toon-memory upgrade # Update to latest version
153
+ npx toon-memory uninstall # Remove from all agents
154
+ ```
155
+
156
+ ### Stats
157
+
158
+ ```bash
159
+ $ npx toon-memory stats
160
+
161
+ 🧠 toon-memory stats
162
+
163
+ 📊 Memory Stats
164
+ ━━━━━━━━━━━━━━━━━━
165
+ Total entries: 45
166
+ ├── decision: 12
167
+ ├── pattern: 18
168
+ ├── bug: 8
169
+ └── knowledge: 7
170
+ Last updated: 2026-07-10
171
+ File size: 12.4 KB
172
+ ```
173
+
174
+ ### Export
175
+
176
+ ```bash
177
+ $ npx toon-memory export
178
+
179
+ 🧠 toon-memory export
180
+
181
+ Exported 45 entries to:
182
+ /path/to/project/toon-memory-export.json
183
+ ```
184
+
185
+ ### Import
186
+
187
+ ```bash
188
+ $ npx toon-memory import backup.json
189
+
190
+ 🧠 toon-memory import
191
+
192
+ Imported 3 new entries
193
+ Skipped 2 duplicates
123
194
  ```
124
195
 
125
196
  ---
package/dist/cli/setup.js CHANGED
@@ -77,6 +77,23 @@ function installOpenCodeTools() {
77
77
  console.log(" Created .opencode/memory/data.toon");
78
78
  }
79
79
  }
80
+ function ensureGitignore() {
81
+ const gitignorePath = join(projectRoot, ".gitignore");
82
+ const entry = ".opencode/memory/";
83
+ if (!existsSync(gitignorePath)) {
84
+ writeFileSync(gitignorePath, `${entry}
85
+ `);
86
+ console.log(" Created .gitignore with memory exclusion");
87
+ return;
88
+ }
89
+ const content = readFileSync(gitignorePath, "utf-8");
90
+ if (!content.includes(entry)) {
91
+ writeFileSync(gitignorePath, `${content.trim()}
92
+ ${entry}
93
+ `);
94
+ console.log(" Added .opencode/memory/ to .gitignore");
95
+ }
96
+ }
80
97
  function installMCPConfig(agent, scope) {
81
98
  const configPath = scope === "global" ? agent.global : agent.local;
82
99
  if (!configPath) {
@@ -139,6 +156,7 @@ function init(scope = "local") {
139
156
  installMCPConfig(agent, scope);
140
157
  console.log("");
141
158
  }
159
+ ensureGitignore();
142
160
  console.log("Done! Restart your agent to use memory tools.\n");
143
161
  }
144
162
  function status() {
@@ -365,6 +383,7 @@ Installing ${scope}ly...
365
383
  installMCPConfig(agent, scope);
366
384
  console.log("");
367
385
  }
386
+ ensureGitignore();
368
387
  console.log("Done! Restart your agent to use memory tools.");
369
388
  console.log("Run 'npx toon-memory uninstall' to remove.\n");
370
389
  rl.close();
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 } } }
@@ -27709,10 +27763,12 @@ server.registerTool(
27709
27763
  description: "Busca en la memoria persistente del proyecto. Devuelve entradas relevantes. Usar ANTES de leer archivos.",
27710
27764
  inputSchema: {
27711
27765
  query: external_exports.string().describe("Texto a buscar"),
27712
- category: external_exports.string().optional().default("").describe("Filtrar por categor\xEDa (vac\xEDo = todos)")
27766
+ category: external_exports.string().optional().default("").describe("Filtrar por categor\xEDa (vac\xEDo = todos)"),
27767
+ from_date: external_exports.string().optional().default("").describe("Fecha inicio filtro (YYYY-MM-DD)"),
27768
+ to_date: external_exports.string().optional().default("").describe("Fecha fin filtro (YYYY-MM-DD)")
27713
27769
  }
27714
27770
  },
27715
- async ({ query, category }) => {
27771
+ async ({ query, category, from_date, to_date }) => {
27716
27772
  const data = readMemory();
27717
27773
  const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"));
27718
27774
  const queryLower = query.toLowerCase();
@@ -27722,6 +27778,8 @@ server.registerTool(
27722
27778
  if (parts.length < 7) return null;
27723
27779
  const [id, cat, key, content, file2, tags, date5] = parts;
27724
27780
  if (category && cat !== category) return null;
27781
+ if (from_date && date5 < from_date) return null;
27782
+ if (to_date && date5 > to_date) return null;
27725
27783
  const searchStr = `${id} ${cat} ${key} ${content} ${file2} ${tags}`.toLowerCase();
27726
27784
  if (!searchStr.includes(queryLower)) return null;
27727
27785
  return { id, cat, key, content, file: file2, tags, date: date5 };
@@ -27849,6 +27907,27 @@ server.registerTool(
27849
27907
  };
27850
27908
  }
27851
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
+ );
27852
27931
  var transport = new StdioServerTransport();
27853
27932
  await server.connect(transport);
27854
27933
  /*! Bundled license information:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toon-memory",
3
- "version": "1.2.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/cli/setup.ts CHANGED
@@ -112,6 +112,24 @@ function installOpenCodeTools(): void {
112
112
  }
113
113
  }
114
114
 
115
+ // Add .opencode/memory/ to .gitignore if not present
116
+ function ensureGitignore(): void {
117
+ const gitignorePath = join(projectRoot, ".gitignore")
118
+ const entry = ".opencode/memory/"
119
+
120
+ if (!existsSync(gitignorePath)) {
121
+ writeFileSync(gitignorePath, `${entry}\n`)
122
+ console.log(" Created .gitignore with memory exclusion")
123
+ return
124
+ }
125
+
126
+ const content = readFileSync(gitignorePath, "utf-8")
127
+ if (!content.includes(entry)) {
128
+ writeFileSync(gitignorePath, `${content.trim()}\n${entry}\n`)
129
+ console.log(" Added .opencode/memory/ to .gitignore")
130
+ }
131
+ }
132
+
115
133
  // Install MCP server config for different agents
116
134
  function installMCPConfig(agent: Agent, scope: string): void {
117
135
  const configPath = scope === "global" ? agent.global : agent.local
@@ -197,6 +215,8 @@ function init(scope: string = "local"): void {
197
215
  console.log("")
198
216
  }
199
217
 
218
+ ensureGitignore()
219
+
200
220
  console.log("Done! Restart your agent to use memory tools.\n")
201
221
  }
202
222
 
@@ -487,6 +507,8 @@ rl.question("Install (1) Local or (2) Global? [1/2]: ", (answer: string) => {
487
507
  console.log("")
488
508
  }
489
509
 
510
+ ensureGitignore()
511
+
490
512
  console.log("Done! Restart your agent to use memory tools.")
491
513
  console.log("Run 'npx toon-memory uninstall' to remove.\n")
492
514
  rl.close()
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 } } }
@@ -83,9 +151,11 @@ server.registerTool(
83
151
  inputSchema: {
84
152
  query: z.string().describe("Texto a buscar"),
85
153
  category: z.string().optional().default("").describe("Filtrar por categoría (vacío = todos)"),
154
+ from_date: z.string().optional().default("").describe("Fecha inicio filtro (YYYY-MM-DD)"),
155
+ to_date: z.string().optional().default("").describe("Fecha fin filtro (YYYY-MM-DD)"),
86
156
  },
87
157
  },
88
- async ({ query, category }) => {
158
+ async ({ query, category, from_date, to_date }) => {
89
159
  const data = readMemory()
90
160
  const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
91
161
  const queryLower = query.toLowerCase()
@@ -97,6 +167,8 @@ server.registerTool(
97
167
  if (parts.length < 7) return null
98
168
  const [id, cat, key, content, file, tags, date] = parts
99
169
  if (category && cat !== category) return null
170
+ if (from_date && date < from_date) return null
171
+ if (to_date && date > to_date) return null
100
172
  const searchStr = `${id} ${cat} ${key} ${content} ${file} ${tags}`.toLowerCase()
101
173
  if (!searchStr.includes(queryLower)) return null
102
174
  return { id, cat, key, content, file, tags, date }
@@ -247,5 +319,28 @@ server.registerTool(
247
319
  }
248
320
  )
249
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
+
250
345
  const transport = new StdioServerTransport()
251
346
  await server.connect(transport)