toon-memory 1.4.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,7 +22,7 @@ AI agents forget everything between sessions. toon-memory fixes this by providin
22
22
 
23
23
  ## Features
24
24
 
25
- - **6 MCP tools** — `memory_remember`, `memory_recall`, `memory_forget`, `memory_stats`, `memory_summary`, `memory_archive`
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
@@ -30,6 +30,8 @@ AI agents forget everything between sessions. toon-memory fixes this by providin
30
30
  - **Auto gitignore** — automatically adds `.opencode/memory/` to `.gitignore`
31
31
  - **Date filtering** — search memory by date range
32
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
33
35
 
34
36
  ---
35
37
 
@@ -91,6 +93,8 @@ memory_remember # Save important decisions
91
93
  | `memory_stats` | View memory state |
92
94
  | `memory_summary` | Save/retrieve file summaries |
93
95
  | `memory_archive` | Archive old entries (>30 days) |
96
+ | `memory_encrypt` | Enable AES-256-GCM encryption |
97
+ | `memory_decrypt` | Disable encryption |
94
98
 
95
99
  ### Date Filtering
96
100
 
@@ -116,6 +120,22 @@ memory_archive()
116
120
  // 📋 Quedan 42 entradas activas
117
121
  ```
118
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
+
119
139
  ---
120
140
 
121
141
  ## How It Works
@@ -149,6 +169,7 @@ npx toon-memory status # Check installation status
149
169
  npx toon-memory stats # View memory statistics
150
170
  npx toon-memory export # Export memory to JSON
151
171
  npx toon-memory import <file> # Import memory from JSON
172
+ npx toon-memory watch [mins] # Auto-backup every N minutes (default: 5)
152
173
  npx toon-memory upgrade # Update to latest version
153
174
  npx toon-memory uninstall # Remove from all agents
154
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,25 +27642,82 @@ 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
27649
  var ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon");
27650
+ var CONFIG_FILE = join(MEMORY_DIR, "config.json");
27650
27651
  var ARCHIVE_DAYS = 30;
27651
- function ensureMemoryFile() {
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() {
27652
27668
  if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
27669
+ }
27670
+ function ensureMemoryFile() {
27671
+ ensureMemoryDir();
27653
27672
  if (!existsSync(MEMORY_FILE)) {
27654
27673
  writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n");
27655
27674
  }
27656
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
+ }
27657
27699
  function readMemory() {
27658
27700
  ensureMemoryFile();
27659
- 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;
27660
27711
  }
27661
27712
  function writeMemory(content) {
27662
27713
  ensureMemoryFile();
27663
- 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
+ }
27664
27721
  }
27665
27722
  function generateId() {
27666
27723
  return randomBytes(4).toString("hex");
@@ -27928,6 +27985,60 @@ server.registerTool(
27928
27985
  };
27929
27986
  }
27930
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
+ );
27931
28042
  var transport = new StdioServerTransport();
27932
28043
  await server.connect(transport);
27933
28044
  /*! Bundled license information:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toon-memory",
3
- "version": "1.4.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,30 +4,107 @@ 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
12
  const ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon")
13
+ const CONFIG_FILE = join(MEMORY_DIR, "config.json")
13
14
  const MAX_ENTRIES = 100
14
15
  const ARCHIVE_DAYS = 30
16
+ const ALGORITHM = "aes-256-gcm"
15
17
 
16
- function ensureMemoryFile() {
18
+ interface MemoryConfig {
19
+ encrypted: boolean
20
+ key?: string
21
+ }
22
+
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() {
17
40
  if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
41
+ }
42
+
43
+ function ensureMemoryFile() {
44
+ ensureMemoryDir()
18
45
  if (!existsSync(MEMORY_FILE)) {
19
46
  writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
20
47
  }
21
48
  }
22
49
 
23
- 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 {
24
83
  ensureMemoryFile()
25
- 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
26
96
  }
27
97
 
28
- function writeMemory(content: string) {
98
+ function writeMemory(content: string): void {
29
99
  ensureMemoryFile()
30
- 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
+ }
31
108
  }
32
109
 
33
110
  function generateId(): string {
@@ -342,5 +419,68 @@ server.registerTool(
342
419
  }
343
420
  )
344
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
+
345
485
  const transport = new StdioServerTransport()
346
486
  await server.connect(transport)