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/README.md +165 -38
- package/bin/toon-memory.js +0 -0
- package/dist/cli/setup.js +146 -5
- package/mcp/server.js +115 -4
- package/package.json +1 -1
- package/src/cli/setup.ts +350 -15
- package/src/mcp/server.ts +337 -6
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|