toon-memory 1.3.0 → 1.5.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,16 @@ 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
+ - **8 MCP tools** — `memory_remember`, `memory_recall`, `memory_forget`, `memory_stats`, `memory_summary`, `memory_archive`, `memory_encrypt`, `memory_decrypt`
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
33
+ - **Encryption** — AES-256-GCM encryption for sensitive data
34
+ - **Watch mode** — auto-backup every N minutes
32
35
 
33
36
  ---
34
37
 
@@ -89,6 +92,9 @@ memory_remember # Save important decisions
89
92
  | `memory_forget` | Remove an entry by key or id |
90
93
  | `memory_stats` | View memory state |
91
94
  | `memory_summary` | Save/retrieve file summaries |
95
+ | `memory_archive` | Archive old entries (>30 days) |
96
+ | `memory_encrypt` | Enable AES-256-GCM encryption |
97
+ | `memory_decrypt` | Disable encryption |
92
98
 
93
99
  ### Date Filtering
94
100
 
@@ -103,6 +109,33 @@ memory_recall({
103
109
  })
104
110
  ```
105
111
 
112
+ ### Auto-Archive
113
+
114
+ Entries older than 30 days are automatically archived to keep memory clean:
115
+
116
+ ```typescript
117
+ // Manually trigger archiving
118
+ memory_archive()
119
+ // 📦 Archivadas 5 entradas antiguas
120
+ // 📋 Quedan 42 entradas activas
121
+ ```
122
+
123
+ ### Encryption
124
+
125
+ Enable AES-256-GCM encryption for sensitive data:
126
+
127
+ ```typescript
128
+ // Enable encryption
129
+ memory_encrypt()
130
+ // 🔐 Encriptación habilitada
131
+ // ⚠️ Guarda esta clave (no se puede recuperar):
132
+ // a1b2c3d4...
133
+
134
+ // Disable encryption
135
+ memory_decrypt({ key: "a1b2c3d4..." })
136
+ // 🔓 Encriptación deshabilitada
137
+ ```
138
+
106
139
  ---
107
140
 
108
141
  ## How It Works
@@ -136,6 +169,7 @@ npx toon-memory status # Check installation status
136
169
  npx toon-memory stats # View memory statistics
137
170
  npx toon-memory export # Export memory to JSON
138
171
  npx toon-memory import <file> # Import memory from JSON
172
+ npx toon-memory watch [mins] # Auto-backup every N minutes (default: 5)
139
173
  npx toon-memory upgrade # Update to latest version
140
174
  npx toon-memory uninstall # Remove from all agents
141
175
  ```
package/dist/cli/setup.js CHANGED
@@ -335,6 +335,46 @@ ${newLines}
335
335
  console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates
336
336
  `);
337
337
  }
338
+ function watch() {
339
+ console.log("\n\u{1F9E0} toon-memory watch\n");
340
+ const memoryFile = join(projectRoot, ".opencode", "memory", "data.toon");
341
+ const backupDir = join(projectRoot, ".opencode", "memory", "backups");
342
+ if (!existsSync(memoryFile)) {
343
+ console.log("Memory not initialized. Run 'npx toon-memory init' first.\n");
344
+ return;
345
+ }
346
+ if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });
347
+ const intervalMinutes = parseInt(args[1]) || 5;
348
+ console.log(`Watching memory file every ${intervalMinutes} minutes...`);
349
+ console.log(`Press Ctrl+C to stop
350
+ `);
351
+ let lastContent = readFileSync(memoryFile, "utf-8");
352
+ let backupCount = 0;
353
+ const backup = () => {
354
+ const currentContent = readFileSync(memoryFile, "utf-8");
355
+ if (currentContent !== lastContent) {
356
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
357
+ const backupFile = join(backupDir, `backup-${timestamp}.toon`);
358
+ writeFileSync(backupFile, currentContent);
359
+ backupCount++;
360
+ console.log(`\u{1F4E6} Backup #${backupCount} created: ${timestamp}`);
361
+ lastContent = currentContent;
362
+ }
363
+ };
364
+ backup();
365
+ const interval = setInterval(backup, intervalMinutes * 60 * 1e3);
366
+ process.on("SIGINT", () => {
367
+ clearInterval(interval);
368
+ console.log(`
369
+ \u2705 Watch stopped. ${backupCount} backups created.
370
+ `);
371
+ process.exit(0);
372
+ });
373
+ process.on("SIGTERM", () => {
374
+ clearInterval(interval);
375
+ process.exit(0);
376
+ });
377
+ }
338
378
  const args = process.argv.slice(2);
339
379
  if (args[0] === "uninstall") {
340
380
  uninstall();
@@ -364,6 +404,10 @@ if (args[0] === "import") {
364
404
  importMemory();
365
405
  process.exit(0);
366
406
  }
407
+ if (args[0] === "watch") {
408
+ watch();
409
+ process.exit(0);
410
+ }
367
411
  const agents = detectAgents();
368
412
  console.log("\n\u{1F9E0} toon-memory installer\n");
369
413
  console.log("Supported agents:");
package/mcp/server.js CHANGED
@@ -27642,27 +27642,138 @@ var StdioServerTransport = class {
27642
27642
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
27643
27643
  import { dirname, join } from "path";
27644
27644
  import { fileURLToPath } from "url";
27645
- import { randomBytes } from "crypto";
27645
+ import { randomBytes, createCipheriv, createDecipheriv } 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
- function ensureMemoryFile() {
27649
+ var ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon");
27650
+ var CONFIG_FILE = join(MEMORY_DIR, "config.json");
27651
+ var ARCHIVE_DAYS = 30;
27652
+ var ALGORITHM = "aes-256-gcm";
27653
+ function loadConfig() {
27654
+ if (!existsSync(CONFIG_FILE)) {
27655
+ return { encrypted: false };
27656
+ }
27657
+ try {
27658
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
27659
+ } catch {
27660
+ return { encrypted: false };
27661
+ }
27662
+ }
27663
+ function saveConfig(config2) {
27664
+ ensureMemoryDir();
27665
+ writeFileSync(CONFIG_FILE, JSON.stringify(config2, null, 2));
27666
+ }
27667
+ function ensureMemoryDir() {
27650
27668
  if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
27669
+ }
27670
+ function ensureMemoryFile() {
27671
+ ensureMemoryDir();
27651
27672
  if (!existsSync(MEMORY_FILE)) {
27652
27673
  writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n");
27653
27674
  }
27654
27675
  }
27676
+ function encrypt(text, key) {
27677
+ const keyBuffer = Buffer.from(key, "hex");
27678
+ const iv = randomBytes(16);
27679
+ const cipher = createCipheriv(ALGORITHM, keyBuffer, iv);
27680
+ let encrypted = cipher.update(text, "utf8", "hex");
27681
+ encrypted += cipher.final("hex");
27682
+ const authTag = cipher.getAuthTag();
27683
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
27684
+ }
27685
+ function decrypt(encryptedData, key) {
27686
+ const keyBuffer = Buffer.from(key, "hex");
27687
+ const [ivHex, authTagHex, encrypted] = encryptedData.split(":");
27688
+ const iv = Buffer.from(ivHex, "hex");
27689
+ const authTag = Buffer.from(authTagHex, "hex");
27690
+ const decipher = createDecipheriv(ALGORITHM, keyBuffer, iv);
27691
+ decipher.setAuthTag(authTag);
27692
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
27693
+ decrypted += decipher.final("utf8");
27694
+ return decrypted;
27695
+ }
27696
+ function generateKey() {
27697
+ return randomBytes(32).toString("hex");
27698
+ }
27655
27699
  function readMemory() {
27656
27700
  ensureMemoryFile();
27657
- return readFileSync(MEMORY_FILE, "utf-8");
27701
+ const config2 = loadConfig();
27702
+ const data = readFileSync(MEMORY_FILE, "utf-8");
27703
+ if (config2.encrypted && config2.key) {
27704
+ try {
27705
+ return decrypt(data, config2.key);
27706
+ } catch {
27707
+ return data;
27708
+ }
27709
+ }
27710
+ return data;
27658
27711
  }
27659
27712
  function writeMemory(content) {
27660
27713
  ensureMemoryFile();
27661
- writeFileSync(MEMORY_FILE, content);
27714
+ const config2 = loadConfig();
27715
+ if (config2.encrypted && config2.key) {
27716
+ const encrypted = encrypt(content, config2.key);
27717
+ writeFileSync(MEMORY_FILE, encrypted);
27718
+ } else {
27719
+ writeFileSync(MEMORY_FILE, content);
27720
+ }
27662
27721
  }
27663
27722
  function generateId() {
27664
27723
  return randomBytes(4).toString("hex");
27665
27724
  }
27725
+ function archiveOldEntries() {
27726
+ const data = readMemory();
27727
+ const lines = data.split("\n");
27728
+ const headerIdx = lines.findIndex((l) => l.startsWith("entries["));
27729
+ if (headerIdx === -1) return { archived: 0, kept: 0 };
27730
+ const today = /* @__PURE__ */ new Date();
27731
+ const cutoff = new Date(today);
27732
+ cutoff.setDate(cutoff.getDate() - ARCHIVE_DAYS);
27733
+ const cutoffStr = cutoff.toISOString().split("T")[0];
27734
+ const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0);
27735
+ const toArchive = [];
27736
+ const toKeep = [];
27737
+ for (const line of entryLines) {
27738
+ const parts = line.trim().split("|");
27739
+ if (parts.length >= 7) {
27740
+ const date5 = parts[6];
27741
+ if (date5 < cutoffStr) {
27742
+ toArchive.push(line.trim());
27743
+ } else {
27744
+ toKeep.push(line.trim());
27745
+ }
27746
+ } else {
27747
+ toKeep.push(line.trim());
27748
+ }
27749
+ }
27750
+ if (toArchive.length === 0) return { archived: 0, kept: toKeep.length };
27751
+ let archiveContent = "";
27752
+ if (existsSync(ARCHIVE_FILE)) {
27753
+ archiveContent = readFileSync(ARCHIVE_FILE, "utf-8");
27754
+ } else {
27755
+ archiveContent = `version: 1
27756
+ archived:
27757
+ `;
27758
+ }
27759
+ const archiveLines = archiveContent.split("\n");
27760
+ let archiveHeaderIdx = archiveLines.findIndex((l) => l.startsWith("archived["));
27761
+ if (archiveHeaderIdx === -1) {
27762
+ archiveLines.push(`archived[0|]{id|category|key|content|file|tags|date}:`);
27763
+ archiveHeaderIdx = archiveLines.length - 1;
27764
+ }
27765
+ const archiveMatch = archiveLines[archiveHeaderIdx].match(/archived\[(\d+)\|/);
27766
+ const archiveCount = archiveMatch ? parseInt(archiveMatch[1]) : 0;
27767
+ for (const entry of toArchive) {
27768
+ archiveLines.splice(archiveHeaderIdx + 1, 0, ` ${entry}`);
27769
+ }
27770
+ archiveLines[archiveHeaderIdx] = archiveLines[archiveHeaderIdx].replace(/archived\[\d+\|/, `[${archiveCount + toArchive.length}|`);
27771
+ writeFileSync(ARCHIVE_FILE, archiveLines.join("\n"));
27772
+ lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${toKeep.length}|`);
27773
+ lines.splice(headerIdx + 1, entryLines.length, ...toKeep.map((l) => ` ${l}`));
27774
+ writeMemory(lines.join("\n"));
27775
+ return { archived: toArchive.length, kept: toKeep.length };
27776
+ }
27666
27777
  var server = new McpServer(
27667
27778
  { name: "toon-memory", version: "1.1.0" },
27668
27779
  { capabilities: { tools: { listChanged: true } } }
@@ -27853,6 +27964,81 @@ server.registerTool(
27853
27964
  };
27854
27965
  }
27855
27966
  );
27967
+ server.registerTool(
27968
+ "memory_archive",
27969
+ {
27970
+ title: "Archive Old Entries",
27971
+ description: "Mover entradas antiguas (>30 d\xEDas) a archive.toon para mantener la memoria limpia.",
27972
+ inputSchema: {}
27973
+ },
27974
+ async () => {
27975
+ const result = archiveOldEntries();
27976
+ if (result.archived === 0) {
27977
+ return { content: [{ type: "text", text: "No hay entradas antiguas para archivar" }] };
27978
+ }
27979
+ return {
27980
+ content: [{
27981
+ type: "text",
27982
+ text: `\u{1F4E6} Archivadas ${result.archived} entradas antiguas
27983
+ \u{1F4CB} Quedan ${result.kept} entradas activas`
27984
+ }]
27985
+ };
27986
+ }
27987
+ );
27988
+ server.registerTool(
27989
+ "memory_encrypt",
27990
+ {
27991
+ title: "Enable Encryption",
27992
+ description: "Habilita encriptaci\xF3n AES-256-GCM para la memoria. La clave se genera autom\xE1ticamente.",
27993
+ inputSchema: {}
27994
+ },
27995
+ async () => {
27996
+ const config2 = loadConfig();
27997
+ if (config2.encrypted) {
27998
+ return { content: [{ type: "text", text: "La encriptaci\xF3n ya est\xE1 habilitada" }] };
27999
+ }
28000
+ const key = generateKey();
28001
+ const data = readFileSync(MEMORY_FILE, "utf-8");
28002
+ const encrypted = encrypt(data, key);
28003
+ writeFileSync(MEMORY_FILE, encrypted);
28004
+ saveConfig({ encrypted: true, key });
28005
+ return {
28006
+ content: [{
28007
+ type: "text",
28008
+ text: `\u{1F510} Encriptaci\xF3n habilitada
28009
+ \u26A0\uFE0F Guarda esta clave (no se puede recuperar):
28010
+ ${key}`
28011
+ }]
28012
+ };
28013
+ }
28014
+ );
28015
+ server.registerTool(
28016
+ "memory_decrypt",
28017
+ {
28018
+ title: "Disable Encryption",
28019
+ description: "Deshabilita la encriptaci\xF3n y decodifica la memoria.",
28020
+ inputSchema: {
28021
+ key: external_exports.string().describe("Clave de encriptaci\xF3n")
28022
+ }
28023
+ },
28024
+ async ({ key }) => {
28025
+ const config2 = loadConfig();
28026
+ if (!config2.encrypted) {
28027
+ return { content: [{ type: "text", text: "La encriptaci\xF3n no est\xE1 habilitada" }] };
28028
+ }
28029
+ try {
28030
+ const data = readFileSync(MEMORY_FILE, "utf-8");
28031
+ const decrypted = decrypt(data, key);
28032
+ writeFileSync(MEMORY_FILE, decrypted);
28033
+ saveConfig({ encrypted: false });
28034
+ return {
28035
+ content: [{ type: "text", text: "\u{1F513} Encriptaci\xF3n deshabilitada" }]
28036
+ };
28037
+ } catch {
28038
+ return { content: [{ type: "text", text: "\u274C Clave incorrecta o datos corruptos" }] };
28039
+ }
28040
+ }
28041
+ );
27856
28042
  var transport = new StdioServerTransport();
27857
28043
  await server.connect(transport);
27858
28044
  /*! 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.5.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
@@ -446,6 +446,59 @@ function importMemory(): void {
446
446
  console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates\n`)
447
447
  }
448
448
 
449
+ // Watch mode - backup memory every N minutes
450
+ function watch(): void {
451
+ console.log("\n🧠 toon-memory watch\n")
452
+
453
+ const memoryFile = join(projectRoot, ".opencode", "memory", "data.toon")
454
+ const backupDir = join(projectRoot, ".opencode", "memory", "backups")
455
+
456
+ if (!existsSync(memoryFile)) {
457
+ console.log("Memory not initialized. Run 'npx toon-memory init' first.\n")
458
+ return
459
+ }
460
+
461
+ if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true })
462
+
463
+ const intervalMinutes = parseInt(args[1]) || 5
464
+ console.log(`Watching memory file every ${intervalMinutes} minutes...`)
465
+ console.log(`Press Ctrl+C to stop\n`)
466
+
467
+ let lastContent = readFileSync(memoryFile, "utf-8")
468
+ let backupCount = 0
469
+
470
+ const backup = () => {
471
+ const currentContent = readFileSync(memoryFile, "utf-8")
472
+
473
+ if (currentContent !== lastContent) {
474
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
475
+ const backupFile = join(backupDir, `backup-${timestamp}.toon`)
476
+ writeFileSync(backupFile, currentContent)
477
+ backupCount++
478
+ console.log(`📦 Backup #${backupCount} created: ${timestamp}`)
479
+ lastContent = currentContent
480
+ }
481
+ }
482
+
483
+ // Initial backup
484
+ backup()
485
+
486
+ // Set interval
487
+ const interval = setInterval(backup, intervalMinutes * 60 * 1000)
488
+
489
+ // Handle Ctrl+C
490
+ process.on("SIGINT", () => {
491
+ clearInterval(interval)
492
+ console.log(`\n✅ Watch stopped. ${backupCount} backups created.\n`)
493
+ process.exit(0)
494
+ })
495
+
496
+ process.on("SIGTERM", () => {
497
+ clearInterval(interval)
498
+ process.exit(0)
499
+ })
500
+ }
501
+
449
502
  // Main
450
503
  const args = process.argv.slice(2)
451
504
 
@@ -484,6 +537,11 @@ if (args[0] === "import") {
484
537
  process.exit(0)
485
538
  }
486
539
 
540
+ if (args[0] === "watch") {
541
+ watch()
542
+ process.exit(0)
543
+ }
544
+
487
545
  const agents = detectAgents()
488
546
  console.log("\n🧠 toon-memory installer\n")
489
547
 
package/src/mcp/server.ts CHANGED
@@ -4,33 +4,178 @@ import { z } from "zod"
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
5
5
  import { dirname, join } from "path"
6
6
  import { fileURLToPath } from "url"
7
- import { randomBytes } from "crypto"
7
+ import { randomBytes, createCipheriv, createDecipheriv } from "crypto"
8
8
 
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 CONFIG_FILE = join(MEMORY_DIR, "config.json")
14
+ const MAX_ENTRIES = 100
15
+ const ARCHIVE_DAYS = 30
16
+ const ALGORITHM = "aes-256-gcm"
17
+
18
+ interface MemoryConfig {
19
+ encrypted: boolean
20
+ key?: string
21
+ }
12
22
 
13
- function ensureMemoryFile() {
23
+ function loadConfig(): MemoryConfig {
24
+ if (!existsSync(CONFIG_FILE)) {
25
+ return { encrypted: false }
26
+ }
27
+ try {
28
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
29
+ } catch {
30
+ return { encrypted: false }
31
+ }
32
+ }
33
+
34
+ function saveConfig(config: MemoryConfig): void {
35
+ ensureMemoryDir()
36
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
37
+ }
38
+
39
+ function ensureMemoryDir() {
14
40
  if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
41
+ }
42
+
43
+ function ensureMemoryFile() {
44
+ ensureMemoryDir()
15
45
  if (!existsSync(MEMORY_FILE)) {
16
46
  writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
17
47
  }
18
48
  }
19
49
 
20
- function readMemory() {
50
+ function encrypt(text: string, key: string): string {
51
+ const keyBuffer = Buffer.from(key, "hex")
52
+ const iv = randomBytes(16)
53
+ const cipher = createCipheriv(ALGORITHM, keyBuffer, iv)
54
+
55
+ let encrypted = cipher.update(text, "utf8", "hex")
56
+ encrypted += cipher.final("hex")
57
+
58
+ const authTag = cipher.getAuthTag()
59
+
60
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`
61
+ }
62
+
63
+ function decrypt(encryptedData: string, key: string): string {
64
+ const keyBuffer = Buffer.from(key, "hex")
65
+ const [ivHex, authTagHex, encrypted] = encryptedData.split(":")
66
+
67
+ const iv = Buffer.from(ivHex, "hex")
68
+ const authTag = Buffer.from(authTagHex, "hex")
69
+ const decipher = createDecipheriv(ALGORITHM, keyBuffer, iv)
70
+ decipher.setAuthTag(authTag)
71
+
72
+ let decrypted = decipher.update(encrypted, "hex", "utf8")
73
+ decrypted += decipher.final("utf8")
74
+
75
+ return decrypted
76
+ }
77
+
78
+ function generateKey(): string {
79
+ return randomBytes(32).toString("hex")
80
+ }
81
+
82
+ function readMemory(): string {
21
83
  ensureMemoryFile()
22
- return readFileSync(MEMORY_FILE, "utf-8")
84
+ const config = loadConfig()
85
+ const data = readFileSync(MEMORY_FILE, "utf-8")
86
+
87
+ if (config.encrypted && config.key) {
88
+ try {
89
+ return decrypt(data, config.key)
90
+ } catch {
91
+ return data
92
+ }
93
+ }
94
+
95
+ return data
23
96
  }
24
97
 
25
- function writeMemory(content: string) {
98
+ function writeMemory(content: string): void {
26
99
  ensureMemoryFile()
27
- writeFileSync(MEMORY_FILE, content)
100
+ const config = loadConfig()
101
+
102
+ if (config.encrypted && config.key) {
103
+ const encrypted = encrypt(content, config.key)
104
+ writeFileSync(MEMORY_FILE, encrypted)
105
+ } else {
106
+ writeFileSync(MEMORY_FILE, content)
107
+ }
28
108
  }
29
109
 
30
110
  function generateId(): string {
31
111
  return randomBytes(4).toString("hex")
32
112
  }
33
113
 
114
+ function archiveOldEntries(): { archived: number; kept: number } {
115
+ const data = readMemory()
116
+ const lines = data.split("\n")
117
+ const headerIdx = lines.findIndex((l) => l.startsWith("entries["))
118
+
119
+ if (headerIdx === -1) return { archived: 0, kept: 0 }
120
+
121
+ const today = new Date()
122
+ const cutoff = new Date(today)
123
+ cutoff.setDate(cutoff.getDate() - ARCHIVE_DAYS)
124
+ const cutoffStr = cutoff.toISOString().split("T")[0]
125
+
126
+ const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0)
127
+ const toArchive: string[] = []
128
+ const toKeep: string[] = []
129
+
130
+ for (const line of entryLines) {
131
+ const parts = line.trim().split("|")
132
+ if (parts.length >= 7) {
133
+ const date = parts[6]
134
+ if (date < cutoffStr) {
135
+ toArchive.push(line.trim())
136
+ } else {
137
+ toKeep.push(line.trim())
138
+ }
139
+ } else {
140
+ toKeep.push(line.trim())
141
+ }
142
+ }
143
+
144
+ if (toArchive.length === 0) return { archived: 0, kept: toKeep.length }
145
+
146
+ // Write archived entries
147
+ let archiveContent = ""
148
+ if (existsSync(ARCHIVE_FILE)) {
149
+ archiveContent = readFileSync(ARCHIVE_FILE, "utf-8")
150
+ } else {
151
+ archiveContent = `version: 1\narchived:\n`
152
+ }
153
+
154
+ const archiveLines = archiveContent.split("\n")
155
+ let archiveHeaderIdx = archiveLines.findIndex((l) => l.startsWith("archived["))
156
+ if (archiveHeaderIdx === -1) {
157
+ archiveLines.push(`archived[0|]{id|category|key|content|file|tags|date}:`)
158
+ archiveHeaderIdx = archiveLines.length - 1
159
+ }
160
+
161
+ const archiveMatch = archiveLines[archiveHeaderIdx].match(/archived\[(\d+)\|/)
162
+ const archiveCount = archiveMatch ? parseInt(archiveMatch[1]) : 0
163
+
164
+ for (const entry of toArchive) {
165
+ archiveLines.splice(archiveHeaderIdx + 1, 0, ` ${entry}`)
166
+ }
167
+ archiveLines[archiveHeaderIdx] = archiveLines[archiveHeaderIdx].replace(/archived\[\d+\|/, `[${archiveCount + toArchive.length}|`)
168
+
169
+ writeFileSync(ARCHIVE_FILE, archiveLines.join("\n"))
170
+
171
+ // Update main file
172
+ lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${toKeep.length}|`)
173
+ lines.splice(headerIdx + 1, entryLines.length, ...toKeep.map((l) => ` ${l}`))
174
+ writeMemory(lines.join("\n"))
175
+
176
+ return { archived: toArchive.length, kept: toKeep.length }
177
+ }
178
+
34
179
  const server = new McpServer(
35
180
  { name: "toon-memory", version: "1.1.0" },
36
181
  { capabilities: { tools: { listChanged: true } } }
@@ -251,5 +396,91 @@ server.registerTool(
251
396
  }
252
397
  )
253
398
 
399
+ server.registerTool(
400
+ "memory_archive",
401
+ {
402
+ title: "Archive Old Entries",
403
+ description: "Mover entradas antiguas (>30 días) a archive.toon para mantener la memoria limpia.",
404
+ inputSchema: {},
405
+ },
406
+ async () => {
407
+ const result = archiveOldEntries()
408
+
409
+ if (result.archived === 0) {
410
+ return { content: [{ type: "text" as const, text: "No hay entradas antiguas para archivar" }] }
411
+ }
412
+
413
+ return {
414
+ content: [{
415
+ type: "text" as const,
416
+ text: `📦 Archivadas ${result.archived} entradas antiguas\n📋 Quedan ${result.kept} entradas activas`
417
+ }],
418
+ }
419
+ }
420
+ )
421
+
422
+ server.registerTool(
423
+ "memory_encrypt",
424
+ {
425
+ title: "Enable Encryption",
426
+ description: "Habilita encriptación AES-256-GCM para la memoria. La clave se genera automáticamente.",
427
+ inputSchema: {},
428
+ },
429
+ async () => {
430
+ const config = loadConfig()
431
+
432
+ if (config.encrypted) {
433
+ return { content: [{ type: "text" as const, text: "La encriptación ya está habilitada" }] }
434
+ }
435
+
436
+ const key = generateKey()
437
+ const data = readFileSync(MEMORY_FILE, "utf-8")
438
+
439
+ const encrypted = encrypt(data, key)
440
+ writeFileSync(MEMORY_FILE, encrypted)
441
+
442
+ saveConfig({ encrypted: true, key })
443
+
444
+ return {
445
+ content: [{
446
+ type: "text" as const,
447
+ text: `🔐 Encriptación habilitada\n⚠️ Guarda esta clave (no se puede recuperar):\n${key}`
448
+ }],
449
+ }
450
+ }
451
+ )
452
+
453
+ server.registerTool(
454
+ "memory_decrypt",
455
+ {
456
+ title: "Disable Encryption",
457
+ description: "Deshabilita la encriptación y decodifica la memoria.",
458
+ inputSchema: {
459
+ key: z.string().describe("Clave de encriptación"),
460
+ },
461
+ },
462
+ async ({ key }) => {
463
+ const config = loadConfig()
464
+
465
+ if (!config.encrypted) {
466
+ return { content: [{ type: "text" as const, text: "La encriptación no está habilitada" }] }
467
+ }
468
+
469
+ try {
470
+ const data = readFileSync(MEMORY_FILE, "utf-8")
471
+ const decrypted = decrypt(data, key)
472
+
473
+ writeFileSync(MEMORY_FILE, decrypted)
474
+ saveConfig({ encrypted: false })
475
+
476
+ return {
477
+ content: [{ type: "text" as const, text: "🔓 Encriptación deshabilitada" }],
478
+ }
479
+ } catch {
480
+ return { content: [{ type: "text" as const, text: "❌ Clave incorrecta o datos corruptos" }] }
481
+ }
482
+ }
483
+ )
484
+
254
485
  const transport = new StdioServerTransport()
255
486
  await server.connect(transport)