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/README.md +157 -51
- package/bin/toon-memory.js +0 -0
- package/dist/cli/setup.js +114 -21
- package/mcp/server.js +11 -11
- package/package.json +1 -1
- package/src/cli/setup.ts +306 -34
- package/src/mcp/server.ts +204 -13
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
|
-
|
|
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
|
-
|
|
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(`
|
|
315
|
+
lines.push(`[0|]`)
|
|
206
316
|
headerIdx = lines.length - 1
|
|
207
317
|
}
|
|
208
318
|
|
|
209
|
-
const match = lines[headerIdx].match(
|
|
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(
|
|
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(
|
|
422
|
+
const match = lines[headerIdx].match(/\[(\d+)\|/)
|
|
293
423
|
const count = match ? parseInt(match[1]) : 0
|
|
294
|
-
lines[headerIdx] = lines[headerIdx].replace(
|
|
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
|
{
|