toon-memory 1.4.0 → 1.6.1

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/src/mcp/server.ts CHANGED
@@ -4,36 +4,209 @@ 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
+
11
+ /** Base directory for memory storage */
10
12
  const MEMORY_DIR = join(process.cwd(), ".opencode", "memory")
13
+
14
+ /** Main memory data file */
11
15
  const MEMORY_FILE = join(MEMORY_DIR, "data.toon")
16
+
17
+ /** Archive file for old entries */
12
18
  const ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon")
19
+
20
+ /** Configuration file for encryption settings */
21
+ const CONFIG_FILE = join(MEMORY_DIR, "config.json")
22
+
23
+ /** Maximum active entries before auto-archive */
13
24
  const MAX_ENTRIES = 100
25
+
26
+ /** Days before entries are archived */
14
27
  const ARCHIVE_DAYS = 30
15
28
 
16
- function ensureMemoryFile() {
29
+ /** Encryption algorithm for AES-256-GCM */
30
+ const ALGORITHM = "aes-256-gcm"
31
+
32
+ /** Memory configuration with encryption settings */
33
+ interface MemoryConfig {
34
+ /** Whether encryption is enabled */
35
+ encrypted: boolean
36
+ /** Encryption key (hex-encoded, 64 chars for AES-256) */
37
+ key?: string
38
+ }
39
+
40
+ /**
41
+ * Load memory configuration from config.json.
42
+ *
43
+ * @returns MemoryConfig object with encryption settings
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const config = loadConfig()
48
+ * if (config.encrypted) {
49
+ * console.log("Encryption enabled")
50
+ * }
51
+ * ```
52
+ */
53
+ function loadConfig(): MemoryConfig {
54
+ if (!existsSync(CONFIG_FILE)) {
55
+ return { encrypted: false }
56
+ }
57
+ try {
58
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))
59
+ } catch {
60
+ return { encrypted: false }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Save memory configuration to config.json.
66
+ *
67
+ * @param config - MemoryConfig to save
68
+ */
69
+ function saveConfig(config: MemoryConfig): void {
70
+ ensureMemoryDir()
71
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
72
+ }
73
+
74
+ /**
75
+ * Ensure memory directory exists, creating it if necessary.
76
+ */
77
+ function ensureMemoryDir(): void {
17
78
  if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
79
+ }
80
+
81
+ /**
82
+ * Ensure memory file exists with default structure.
83
+ */
84
+ function ensureMemoryFile(): void {
85
+ ensureMemoryDir()
18
86
  if (!existsSync(MEMORY_FILE)) {
19
87
  writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
20
88
  }
21
89
  }
22
90
 
23
- function readMemory() {
91
+ /**
92
+ * Encrypt text using AES-256-GCM.
93
+ *
94
+ * @param text - Plain text to encrypt
95
+ * @param key - Hex-encoded encryption key (64 chars)
96
+ * @returns Encrypted string in format "iv:authTag:ciphertext"
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const encrypted = encrypt("my secret", key)
101
+ * // "a1b2c3d4...:e5f6g7h8...:i9j0k1l2..."
102
+ * ```
103
+ */
104
+ function encrypt(text: string, key: string): string {
105
+ const keyBuffer = Buffer.from(key, "hex")
106
+ const iv = randomBytes(16)
107
+ const cipher = createCipheriv(ALGORITHM, keyBuffer, iv)
108
+
109
+ let encrypted = cipher.update(text, "utf8", "hex")
110
+ encrypted += cipher.final("hex")
111
+
112
+ const authTag = cipher.getAuthTag()
113
+
114
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`
115
+ }
116
+
117
+ /**
118
+ * Decrypt AES-256-GCM encrypted data.
119
+ *
120
+ * @param encryptedData - Encrypted string in format "iv:authTag:ciphertext"
121
+ * @param key - Hex-encoded encryption key (64 chars)
122
+ * @returns Decrypted plain text
123
+ * @throws If key is invalid or data is corrupted
124
+ */
125
+ function decrypt(encryptedData: string, key: string): string {
126
+ const keyBuffer = Buffer.from(key, "hex")
127
+ const [ivHex, authTagHex, encrypted] = encryptedData.split(":")
128
+
129
+ const iv = Buffer.from(ivHex, "hex")
130
+ const authTag = Buffer.from(authTagHex, "hex")
131
+ const decipher = createDecipheriv(ALGORITHM, keyBuffer, iv)
132
+ decipher.setAuthTag(authTag)
133
+
134
+ let decrypted = decipher.update(encrypted, "hex", "utf8")
135
+ decrypted += decipher.final("utf8")
136
+
137
+ return decrypted
138
+ }
139
+
140
+ /**
141
+ * Generate a random 256-bit encryption key.
142
+ *
143
+ * @returns Hex-encoded key (64 chars)
144
+ */
145
+ function generateKey(): string {
146
+ return randomBytes(32).toString("hex")
147
+ }
148
+
149
+ /**
150
+ * Read memory file, decrypting if encryption is enabled.
151
+ *
152
+ * @returns Memory content as string (TOON format)
153
+ */
154
+ function readMemory(): string {
24
155
  ensureMemoryFile()
25
- return readFileSync(MEMORY_FILE, "utf-8")
156
+ const config = loadConfig()
157
+ const data = readFileSync(MEMORY_FILE, "utf-8")
158
+
159
+ if (config.encrypted && config.key) {
160
+ try {
161
+ return decrypt(data, config.key)
162
+ } catch {
163
+ return data
164
+ }
165
+ }
166
+
167
+ return data
26
168
  }
27
169
 
28
- function writeMemory(content: string) {
170
+ /**
171
+ * Write content to memory file, encrypting if encryption is enabled.
172
+ *
173
+ * @param content - Memory content to write (TOON format)
174
+ */
175
+ function writeMemory(content: string): void {
29
176
  ensureMemoryFile()
30
- writeFileSync(MEMORY_FILE, content)
177
+ const config = loadConfig()
178
+
179
+ if (config.encrypted && config.key) {
180
+ const encrypted = encrypt(content, config.key)
181
+ writeFileSync(MEMORY_FILE, encrypted)
182
+ } else {
183
+ writeFileSync(MEMORY_FILE, content)
184
+ }
31
185
  }
32
186
 
187
+ /**
188
+ * Generate a random 8-character hex ID for memory entries.
189
+ *
190
+ * @returns Hex string (8 chars)
191
+ */
33
192
  function generateId(): string {
34
193
  return randomBytes(4).toString("hex")
35
194
  }
36
195
 
196
+ /**
197
+ * Archive entries older than ARCHIVE_DAYS to archive.toon.
198
+ *
199
+ * Moves old entries from data.toon to archive.toon to keep
200
+ * the active memory file manageable.
201
+ *
202
+ * @returns Object with counts of archived and kept entries
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * const result = archiveOldEntries()
207
+ * console.log(`Archived: ${result.archived}, Kept: ${result.kept}`)
208
+ * ```
209
+ */
37
210
  function archiveOldEntries(): { archived: number; kept: number } {
38
211
  const data = readMemory()
39
212
  const lines = data.split("\n")
@@ -104,6 +277,20 @@ const server = new McpServer(
104
277
  { capabilities: { tools: { listChanged: true } } }
105
278
  )
106
279
 
280
+ /**
281
+ * Register the memory_remember tool.
282
+ * Saves a fact to persistent memory (decisions, patterns, bugs, knowledge).
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * // From MCP tool call:
287
+ * await callTool("memory_remember", {
288
+ * category: "decision",
289
+ * key: "use-ts-for-mcp",
290
+ * content: "Using TypeScript for MCP server implementation"
291
+ * })
292
+ * ```
293
+ */
107
294
  server.registerTool(
108
295
  "memory_remember",
109
296
  {
@@ -143,6 +330,17 @@ server.registerTool(
143
330
  }
144
331
  )
145
332
 
333
+ /**
334
+ * Register the memory_recall tool.
335
+ * Search persistent memory for relevant entries by text, category, or date range.
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * // From MCP tool call:
340
+ * await callTool("memory_recall", { query: "risk engine" })
341
+ * await callTool("memory_recall", { query: "redis", category: "bug" })
342
+ * ```
343
+ */
146
344
  server.registerTool(
147
345
  "memory_recall",
148
346
  {
@@ -187,6 +385,15 @@ server.registerTool(
187
385
  }
188
386
  )
189
387
 
388
+ /**
389
+ * Register the memory_forget tool.
390
+ * Delete a memory entry by key or ID.
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * await callTool("memory_forget", { key: "old-decision" })
395
+ * ```
396
+ */
190
397
  server.registerTool(
191
398
  "memory_forget",
192
399
  {
@@ -224,6 +431,15 @@ server.registerTool(
224
431
  }
225
432
  )
226
433
 
434
+ /**
435
+ * Register the memory_stats tool.
436
+ * Display memory statistics including entry counts and categories.
437
+ *
438
+ * @example
439
+ * ```typescript
440
+ * await callTool("memory_stats", {})
441
+ * ```
442
+ */
227
443
  server.registerTool(
228
444
  "memory_stats",
229
445
  {
@@ -263,6 +479,26 @@ server.registerTool(
263
479
  }
264
480
  )
265
481
 
482
+ /**
483
+ * Register the memory_summary tool.
484
+ * Get or set file summaries to save tokens when reading large files.
485
+ *
486
+ * @example
487
+ * ```typescript
488
+ * // Set a summary
489
+ * await callTool("memory_summary", {
490
+ * action: "set",
491
+ * file: "src/complex.ts",
492
+ * summary: "Complex module handling X, Y, Z"
493
+ * })
494
+ *
495
+ * // Get a summary
496
+ * await callTool("memory_summary", {
497
+ * action: "get",
498
+ * file: "src/complex.ts"
499
+ * })
500
+ * ```
501
+ */
266
502
  server.registerTool(
267
503
  "memory_summary",
268
504
  {
@@ -319,6 +555,16 @@ server.registerTool(
319
555
  }
320
556
  )
321
557
 
558
+ /**
559
+ * Register the memory_archive tool.
560
+ * Archive old entries (>30 days) to archive.toon to keep memory clean.
561
+ *
562
+ * @example
563
+ * ```typescript
564
+ * await callTool("memory_archive", {})
565
+ * // "📦 Archivadas 5 entradas antiguas"
566
+ * ```
567
+ */
322
568
  server.registerTool(
323
569
  "memory_archive",
324
570
  {
@@ -342,5 +588,90 @@ server.registerTool(
342
588
  }
343
589
  )
344
590
 
591
+ /**
592
+ * Register the memory_encrypt tool.
593
+ * Enable AES-256-GCM encryption for the memory file.
594
+ *
595
+ * ⚠️ Warning: Save the encryption key! It cannot be recovered.
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * await callTool("memory_encrypt", {})
600
+ * // "🔐 Encriptación habilitada\n⚠️ Guarda esta clave: ..."
601
+ * ```
602
+ */
603
+ server.registerTool(
604
+ "memory_encrypt",
605
+ {
606
+ title: "Enable Encryption",
607
+ description: "Habilita encriptación AES-256-GCM para la memoria. La clave se genera automáticamente.",
608
+ inputSchema: {},
609
+ },
610
+ async () => {
611
+ const config = loadConfig()
612
+
613
+ if (config.encrypted) {
614
+ return { content: [{ type: "text" as const, text: "La encriptación ya está habilitada" }] }
615
+ }
616
+
617
+ const key = generateKey()
618
+ const data = readFileSync(MEMORY_FILE, "utf-8")
619
+
620
+ const encrypted = encrypt(data, key)
621
+ writeFileSync(MEMORY_FILE, encrypted)
622
+
623
+ saveConfig({ encrypted: true, key })
624
+
625
+ return {
626
+ content: [{
627
+ type: "text" as const,
628
+ text: `🔐 Encriptación habilitada\n⚠️ Guarda esta clave (no se puede recuperar):\n${key}`
629
+ }],
630
+ }
631
+ }
632
+ )
633
+
634
+ /**
635
+ * Register the memory_decrypt tool.
636
+ * Disable encryption and decrypt the memory file.
637
+ *
638
+ * @example
639
+ * ```typescript
640
+ * await callTool("memory_decrypt", { key: "your-encryption-key" })
641
+ * // "🔓 Encriptación deshabilitada"
642
+ * ```
643
+ */
644
+ server.registerTool(
645
+ "memory_decrypt",
646
+ {
647
+ title: "Disable Encryption",
648
+ description: "Deshabilita la encriptación y decodifica la memoria.",
649
+ inputSchema: {
650
+ key: z.string().describe("Clave de encriptación"),
651
+ },
652
+ },
653
+ async ({ key }) => {
654
+ const config = loadConfig()
655
+
656
+ if (!config.encrypted) {
657
+ return { content: [{ type: "text" as const, text: "La encriptación no está habilitada" }] }
658
+ }
659
+
660
+ try {
661
+ const data = readFileSync(MEMORY_FILE, "utf-8")
662
+ const decrypted = decrypt(data, key)
663
+
664
+ writeFileSync(MEMORY_FILE, decrypted)
665
+ saveConfig({ encrypted: false })
666
+
667
+ return {
668
+ content: [{ type: "text" as const, text: "🔓 Encriptación deshabilitada" }],
669
+ }
670
+ } catch {
671
+ return { content: [{ type: "text" as const, text: "❌ Clave incorrecta o datos corruptos" }] }
672
+ }
673
+ }
674
+ )
675
+
345
676
  const transport = new StdioServerTransport()
346
677
  await server.connect(transport)