toon-memory 1.5.0 → 1.6.2

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
@@ -7,19 +7,49 @@ import { fileURLToPath } from "url"
7
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 */
13
21
  const CONFIG_FILE = join(MEMORY_DIR, "config.json")
22
+
23
+ /** Maximum active entries before auto-archive */
14
24
  const MAX_ENTRIES = 100
25
+
26
+ /** Days before entries are archived */
15
27
  const ARCHIVE_DAYS = 30
28
+
29
+ /** Encryption algorithm for AES-256-GCM */
16
30
  const ALGORITHM = "aes-256-gcm"
17
31
 
32
+ /** Memory configuration with encryption settings */
18
33
  interface MemoryConfig {
34
+ /** Whether encryption is enabled */
19
35
  encrypted: boolean
36
+ /** Encryption key (hex-encoded, 64 chars for AES-256) */
20
37
  key?: string
21
38
  }
22
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
+ */
23
53
  function loadConfig(): MemoryConfig {
24
54
  if (!existsSync(CONFIG_FILE)) {
25
55
  return { encrypted: false }
@@ -31,22 +61,46 @@ function loadConfig(): MemoryConfig {
31
61
  }
32
62
  }
33
63
 
64
+ /**
65
+ * Save memory configuration to config.json.
66
+ *
67
+ * @param config - MemoryConfig to save
68
+ */
34
69
  function saveConfig(config: MemoryConfig): void {
35
70
  ensureMemoryDir()
36
71
  writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
37
72
  }
38
73
 
39
- function ensureMemoryDir() {
74
+ /**
75
+ * Ensure memory directory exists, creating it if necessary.
76
+ */
77
+ function ensureMemoryDir(): void {
40
78
  if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
41
79
  }
42
80
 
43
- function ensureMemoryFile() {
81
+ /**
82
+ * Ensure memory file exists with default structure.
83
+ */
84
+ function ensureMemoryFile(): void {
44
85
  ensureMemoryDir()
45
86
  if (!existsSync(MEMORY_FILE)) {
46
87
  writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
47
88
  }
48
89
  }
49
90
 
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
+ */
50
104
  function encrypt(text: string, key: string): string {
51
105
  const keyBuffer = Buffer.from(key, "hex")
52
106
  const iv = randomBytes(16)
@@ -60,6 +114,14 @@ function encrypt(text: string, key: string): string {
60
114
  return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`
61
115
  }
62
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
+ */
63
125
  function decrypt(encryptedData: string, key: string): string {
64
126
  const keyBuffer = Buffer.from(key, "hex")
65
127
  const [ivHex, authTagHex, encrypted] = encryptedData.split(":")
@@ -75,10 +137,20 @@ function decrypt(encryptedData: string, key: string): string {
75
137
  return decrypted
76
138
  }
77
139
 
140
+ /**
141
+ * Generate a random 256-bit encryption key.
142
+ *
143
+ * @returns Hex-encoded key (64 chars)
144
+ */
78
145
  function generateKey(): string {
79
146
  return randomBytes(32).toString("hex")
80
147
  }
81
148
 
149
+ /**
150
+ * Read memory file, decrypting if encryption is enabled.
151
+ *
152
+ * @returns Memory content as string (TOON format)
153
+ */
82
154
  function readMemory(): string {
83
155
  ensureMemoryFile()
84
156
  const config = loadConfig()
@@ -95,6 +167,11 @@ function readMemory(): string {
95
167
  return data
96
168
  }
97
169
 
170
+ /**
171
+ * Write content to memory file, encrypting if encryption is enabled.
172
+ *
173
+ * @param content - Memory content to write (TOON format)
174
+ */
98
175
  function writeMemory(content: string): void {
99
176
  ensureMemoryFile()
100
177
  const config = loadConfig()
@@ -107,10 +184,29 @@ function writeMemory(content: string): void {
107
184
  }
108
185
  }
109
186
 
187
+ /**
188
+ * Generate a random 8-character hex ID for memory entries.
189
+ *
190
+ * @returns Hex string (8 chars)
191
+ */
110
192
  function generateId(): string {
111
193
  return randomBytes(4).toString("hex")
112
194
  }
113
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
+ */
114
210
  function archiveOldEntries(): { archived: number; kept: number } {
115
211
  const data = readMemory()
116
212
  const lines = data.split("\n")
@@ -181,6 +277,20 @@ const server = new McpServer(
181
277
  { capabilities: { tools: { listChanged: true } } }
182
278
  )
183
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
+ */
184
294
  server.registerTool(
185
295
  "memory_remember",
186
296
  {
@@ -200,18 +310,18 @@ server.registerTool(
200
310
  const date = new Date().toISOString().split("T")[0]
201
311
  const lines = data.split("\n")
202
312
 
203
- let headerIdx = lines.findIndex((l) => l.startsWith("entries["))
313
+ let headerIdx = lines.findIndex((l) => l.startsWith("entries[") || /^\[\d+\|]/.test(l))
204
314
  if (headerIdx === -1) {
205
- lines.push(`entries[0|]{id|category|key|content|file|tags|date}:`)
315
+ lines.push(`[0|]`)
206
316
  headerIdx = lines.length - 1
207
317
  }
208
318
 
209
- const match = lines[headerIdx].match(/entries\[(\d+)\|/)
319
+ const match = lines[headerIdx].match(/\[(\d+)\|/)
210
320
  const count = match ? parseInt(match[1]) : 0
211
321
  const newEntry = `${id}|${category}|${key}|${content}|${file || ""}|${tags || ""}|${date}`
212
322
 
213
323
  lines.splice(headerIdx + 1, 0, ` ${newEntry}`)
214
- lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${count + 1}|`)
324
+ lines[headerIdx] = lines[headerIdx].replace(/\[\d+\|/, `[${count + 1}|`)
215
325
 
216
326
  writeMemory(lines.join("\n"))
217
327
  return {
@@ -220,6 +330,17 @@ server.registerTool(
220
330
  }
221
331
  )
222
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
+ */
223
344
  server.registerTool(
224
345
  "memory_recall",
225
346
  {
@@ -234,7 +355,7 @@ server.registerTool(
234
355
  },
235
356
  async ({ query, category, from_date, to_date }) => {
236
357
  const data = readMemory()
237
- const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
358
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith("summaries:"))
238
359
  const queryLower = query.toLowerCase()
239
360
 
240
361
  const results = lines
@@ -264,6 +385,15 @@ server.registerTool(
264
385
  }
265
386
  )
266
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
+ */
267
397
  server.registerTool(
268
398
  "memory_forget",
269
399
  {
@@ -276,22 +406,22 @@ server.registerTool(
276
406
  async ({ key }) => {
277
407
  const data = readMemory()
278
408
  const lines = data.split("\n")
279
- const headerIdx = lines.findIndex((l) => l.startsWith("entries["))
409
+ const headerIdx = lines.findIndex((l) => l.startsWith("entries[") || /^\[\d+\|]/.test(l))
280
410
 
281
411
  if (headerIdx === -1) {
282
412
  return { content: [{ type: "text" as const, text: "No hay entradas en memoria" }] }
283
413
  }
284
414
 
285
- const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0)
415
+ const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0 && !l.startsWith("summaries:"))
286
416
  const filtered = entryLines.filter((l) => {
287
417
  const parts = l.trim().split("|")
288
418
  return parts[0] !== key && parts[2] !== key
289
419
  })
290
420
 
291
421
  const removed = entryLines.length - filtered.length
292
- const match = lines[headerIdx].match(/entries\[(\d+)\|/)
422
+ const match = lines[headerIdx].match(/\[(\d+)\|/)
293
423
  const count = match ? parseInt(match[1]) : 0
294
- lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${count - removed}|`)
424
+ lines[headerIdx] = lines[headerIdx].replace(/\[\d+\|/, `[${count - removed}|`)
295
425
  lines.splice(headerIdx + 1, entryLines.length, ...filtered.map((l) => ` ${l.trim()}`))
296
426
 
297
427
  writeMemory(lines.join("\n"))
@@ -301,6 +431,15 @@ server.registerTool(
301
431
  }
302
432
  )
303
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
+ */
304
443
  server.registerTool(
305
444
  "memory_stats",
306
445
  {
@@ -310,7 +449,7 @@ server.registerTool(
310
449
  },
311
450
  async () => {
312
451
  const data = readMemory()
313
- const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|"))
452
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"))
314
453
  const entries = lines.map((l) => {
315
454
  const parts = l.trim().split("|")
316
455
  return { category: parts[1] || "unknown" }
@@ -321,7 +460,7 @@ server.registerTool(
321
460
  byCategory[e.category] = (byCategory[e.category] || 0) + 1
322
461
  }
323
462
 
324
- const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries"))
463
+ const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries") && !/^\[\d+\|]/.test(l))
325
464
  const stats = [
326
465
  `Entradas totales: ${entries.length}`,
327
466
  `Resúmenes de archivos: ${summaryLines.length}`,
@@ -340,6 +479,26 @@ server.registerTool(
340
479
  }
341
480
  )
342
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
+ */
343
502
  server.registerTool(
344
503
  "memory_summary",
345
504
  {
@@ -396,6 +555,16 @@ server.registerTool(
396
555
  }
397
556
  )
398
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
+ */
399
568
  server.registerTool(
400
569
  "memory_archive",
401
570
  {
@@ -419,6 +588,18 @@ server.registerTool(
419
588
  }
420
589
  )
421
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
+ */
422
603
  server.registerTool(
423
604
  "memory_encrypt",
424
605
  {
@@ -450,6 +631,16 @@ server.registerTool(
450
631
  }
451
632
  )
452
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
+ */
453
644
  server.registerTool(
454
645
  "memory_decrypt",
455
646
  {