toon-memory 1.3.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 +35 -1
- package/dist/cli/setup.js +44 -0
- package/mcp/server.js +190 -4
- package/package.json +1 -1
- package/src/cli/setup.ts +58 -0
- package/src/mcp/server.ts +237 -6
package/README.md
CHANGED
|
@@ -22,13 +22,16 @@ AI agents forget everything between sessions. toon-memory fixes this by providin
|
|
|
22
22
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
|
-
- **
|
|
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
|
|
29
29
|
- **Zero config** — just install and use
|
|
30
30
|
- **Auto gitignore** — automatically adds `.opencode/memory/` to `.gitignore`
|
|
31
31
|
- **Date filtering** — search memory by date range
|
|
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
|
|
32
35
|
|
|
33
36
|
---
|
|
34
37
|
|
|
@@ -89,6 +92,9 @@ memory_remember # Save important decisions
|
|
|
89
92
|
| `memory_forget` | Remove an entry by key or id |
|
|
90
93
|
| `memory_stats` | View memory state |
|
|
91
94
|
| `memory_summary` | Save/retrieve file summaries |
|
|
95
|
+
| `memory_archive` | Archive old entries (>30 days) |
|
|
96
|
+
| `memory_encrypt` | Enable AES-256-GCM encryption |
|
|
97
|
+
| `memory_decrypt` | Disable encryption |
|
|
92
98
|
|
|
93
99
|
### Date Filtering
|
|
94
100
|
|
|
@@ -103,6 +109,33 @@ memory_recall({
|
|
|
103
109
|
})
|
|
104
110
|
```
|
|
105
111
|
|
|
112
|
+
### Auto-Archive
|
|
113
|
+
|
|
114
|
+
Entries older than 30 days are automatically archived to keep memory clean:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Manually trigger archiving
|
|
118
|
+
memory_archive()
|
|
119
|
+
// 📦 Archivadas 5 entradas antiguas
|
|
120
|
+
// 📋 Quedan 42 entradas activas
|
|
121
|
+
```
|
|
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
|
+
|
|
106
139
|
---
|
|
107
140
|
|
|
108
141
|
## How It Works
|
|
@@ -136,6 +169,7 @@ npx toon-memory status # Check installation status
|
|
|
136
169
|
npx toon-memory stats # View memory statistics
|
|
137
170
|
npx toon-memory export # Export memory to JSON
|
|
138
171
|
npx toon-memory import <file> # Import memory from JSON
|
|
172
|
+
npx toon-memory watch [mins] # Auto-backup every N minutes (default: 5)
|
|
139
173
|
npx toon-memory upgrade # Update to latest version
|
|
140
174
|
npx toon-memory uninstall # Remove from all agents
|
|
141
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,27 +27642,138 @@ 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");
|
|
27651
|
+
var ARCHIVE_DAYS = 30;
|
|
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() {
|
|
27650
27668
|
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
|
|
27669
|
+
}
|
|
27670
|
+
function ensureMemoryFile() {
|
|
27671
|
+
ensureMemoryDir();
|
|
27651
27672
|
if (!existsSync(MEMORY_FILE)) {
|
|
27652
27673
|
writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n");
|
|
27653
27674
|
}
|
|
27654
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
|
+
}
|
|
27655
27699
|
function readMemory() {
|
|
27656
27700
|
ensureMemoryFile();
|
|
27657
|
-
|
|
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;
|
|
27658
27711
|
}
|
|
27659
27712
|
function writeMemory(content) {
|
|
27660
27713
|
ensureMemoryFile();
|
|
27661
|
-
|
|
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
|
+
}
|
|
27662
27721
|
}
|
|
27663
27722
|
function generateId() {
|
|
27664
27723
|
return randomBytes(4).toString("hex");
|
|
27665
27724
|
}
|
|
27725
|
+
function archiveOldEntries() {
|
|
27726
|
+
const data = readMemory();
|
|
27727
|
+
const lines = data.split("\n");
|
|
27728
|
+
const headerIdx = lines.findIndex((l) => l.startsWith("entries["));
|
|
27729
|
+
if (headerIdx === -1) return { archived: 0, kept: 0 };
|
|
27730
|
+
const today = /* @__PURE__ */ new Date();
|
|
27731
|
+
const cutoff = new Date(today);
|
|
27732
|
+
cutoff.setDate(cutoff.getDate() - ARCHIVE_DAYS);
|
|
27733
|
+
const cutoffStr = cutoff.toISOString().split("T")[0];
|
|
27734
|
+
const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0);
|
|
27735
|
+
const toArchive = [];
|
|
27736
|
+
const toKeep = [];
|
|
27737
|
+
for (const line of entryLines) {
|
|
27738
|
+
const parts = line.trim().split("|");
|
|
27739
|
+
if (parts.length >= 7) {
|
|
27740
|
+
const date5 = parts[6];
|
|
27741
|
+
if (date5 < cutoffStr) {
|
|
27742
|
+
toArchive.push(line.trim());
|
|
27743
|
+
} else {
|
|
27744
|
+
toKeep.push(line.trim());
|
|
27745
|
+
}
|
|
27746
|
+
} else {
|
|
27747
|
+
toKeep.push(line.trim());
|
|
27748
|
+
}
|
|
27749
|
+
}
|
|
27750
|
+
if (toArchive.length === 0) return { archived: 0, kept: toKeep.length };
|
|
27751
|
+
let archiveContent = "";
|
|
27752
|
+
if (existsSync(ARCHIVE_FILE)) {
|
|
27753
|
+
archiveContent = readFileSync(ARCHIVE_FILE, "utf-8");
|
|
27754
|
+
} else {
|
|
27755
|
+
archiveContent = `version: 1
|
|
27756
|
+
archived:
|
|
27757
|
+
`;
|
|
27758
|
+
}
|
|
27759
|
+
const archiveLines = archiveContent.split("\n");
|
|
27760
|
+
let archiveHeaderIdx = archiveLines.findIndex((l) => l.startsWith("archived["));
|
|
27761
|
+
if (archiveHeaderIdx === -1) {
|
|
27762
|
+
archiveLines.push(`archived[0|]{id|category|key|content|file|tags|date}:`);
|
|
27763
|
+
archiveHeaderIdx = archiveLines.length - 1;
|
|
27764
|
+
}
|
|
27765
|
+
const archiveMatch = archiveLines[archiveHeaderIdx].match(/archived\[(\d+)\|/);
|
|
27766
|
+
const archiveCount = archiveMatch ? parseInt(archiveMatch[1]) : 0;
|
|
27767
|
+
for (const entry of toArchive) {
|
|
27768
|
+
archiveLines.splice(archiveHeaderIdx + 1, 0, ` ${entry}`);
|
|
27769
|
+
}
|
|
27770
|
+
archiveLines[archiveHeaderIdx] = archiveLines[archiveHeaderIdx].replace(/archived\[\d+\|/, `[${archiveCount + toArchive.length}|`);
|
|
27771
|
+
writeFileSync(ARCHIVE_FILE, archiveLines.join("\n"));
|
|
27772
|
+
lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${toKeep.length}|`);
|
|
27773
|
+
lines.splice(headerIdx + 1, entryLines.length, ...toKeep.map((l) => ` ${l}`));
|
|
27774
|
+
writeMemory(lines.join("\n"));
|
|
27775
|
+
return { archived: toArchive.length, kept: toKeep.length };
|
|
27776
|
+
}
|
|
27666
27777
|
var server = new McpServer(
|
|
27667
27778
|
{ name: "toon-memory", version: "1.1.0" },
|
|
27668
27779
|
{ capabilities: { tools: { listChanged: true } } }
|
|
@@ -27853,6 +27964,81 @@ server.registerTool(
|
|
|
27853
27964
|
};
|
|
27854
27965
|
}
|
|
27855
27966
|
);
|
|
27967
|
+
server.registerTool(
|
|
27968
|
+
"memory_archive",
|
|
27969
|
+
{
|
|
27970
|
+
title: "Archive Old Entries",
|
|
27971
|
+
description: "Mover entradas antiguas (>30 d\xEDas) a archive.toon para mantener la memoria limpia.",
|
|
27972
|
+
inputSchema: {}
|
|
27973
|
+
},
|
|
27974
|
+
async () => {
|
|
27975
|
+
const result = archiveOldEntries();
|
|
27976
|
+
if (result.archived === 0) {
|
|
27977
|
+
return { content: [{ type: "text", text: "No hay entradas antiguas para archivar" }] };
|
|
27978
|
+
}
|
|
27979
|
+
return {
|
|
27980
|
+
content: [{
|
|
27981
|
+
type: "text",
|
|
27982
|
+
text: `\u{1F4E6} Archivadas ${result.archived} entradas antiguas
|
|
27983
|
+
\u{1F4CB} Quedan ${result.kept} entradas activas`
|
|
27984
|
+
}]
|
|
27985
|
+
};
|
|
27986
|
+
}
|
|
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
|
+
);
|
|
27856
28042
|
var transport = new StdioServerTransport();
|
|
27857
28043
|
await server.connect(transport);
|
|
27858
28044
|
/*! Bundled license information:
|
package/package.json
CHANGED
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,33 +4,178 @@ 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
|
+
const ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon")
|
|
13
|
+
const CONFIG_FILE = join(MEMORY_DIR, "config.json")
|
|
14
|
+
const MAX_ENTRIES = 100
|
|
15
|
+
const ARCHIVE_DAYS = 30
|
|
16
|
+
const ALGORITHM = "aes-256-gcm"
|
|
17
|
+
|
|
18
|
+
interface MemoryConfig {
|
|
19
|
+
encrypted: boolean
|
|
20
|
+
key?: string
|
|
21
|
+
}
|
|
12
22
|
|
|
13
|
-
function
|
|
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() {
|
|
14
40
|
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ensureMemoryFile() {
|
|
44
|
+
ensureMemoryDir()
|
|
15
45
|
if (!existsSync(MEMORY_FILE)) {
|
|
16
46
|
writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
|
|
17
47
|
}
|
|
18
48
|
}
|
|
19
49
|
|
|
20
|
-
function
|
|
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 {
|
|
21
83
|
ensureMemoryFile()
|
|
22
|
-
|
|
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
|
|
23
96
|
}
|
|
24
97
|
|
|
25
|
-
function writeMemory(content: string) {
|
|
98
|
+
function writeMemory(content: string): void {
|
|
26
99
|
ensureMemoryFile()
|
|
27
|
-
|
|
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
|
+
}
|
|
28
108
|
}
|
|
29
109
|
|
|
30
110
|
function generateId(): string {
|
|
31
111
|
return randomBytes(4).toString("hex")
|
|
32
112
|
}
|
|
33
113
|
|
|
114
|
+
function archiveOldEntries(): { archived: number; kept: number } {
|
|
115
|
+
const data = readMemory()
|
|
116
|
+
const lines = data.split("\n")
|
|
117
|
+
const headerIdx = lines.findIndex((l) => l.startsWith("entries["))
|
|
118
|
+
|
|
119
|
+
if (headerIdx === -1) return { archived: 0, kept: 0 }
|
|
120
|
+
|
|
121
|
+
const today = new Date()
|
|
122
|
+
const cutoff = new Date(today)
|
|
123
|
+
cutoff.setDate(cutoff.getDate() - ARCHIVE_DAYS)
|
|
124
|
+
const cutoffStr = cutoff.toISOString().split("T")[0]
|
|
125
|
+
|
|
126
|
+
const entryLines = lines.slice(headerIdx + 1).filter((l) => l.trim().length > 0)
|
|
127
|
+
const toArchive: string[] = []
|
|
128
|
+
const toKeep: string[] = []
|
|
129
|
+
|
|
130
|
+
for (const line of entryLines) {
|
|
131
|
+
const parts = line.trim().split("|")
|
|
132
|
+
if (parts.length >= 7) {
|
|
133
|
+
const date = parts[6]
|
|
134
|
+
if (date < cutoffStr) {
|
|
135
|
+
toArchive.push(line.trim())
|
|
136
|
+
} else {
|
|
137
|
+
toKeep.push(line.trim())
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
toKeep.push(line.trim())
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (toArchive.length === 0) return { archived: 0, kept: toKeep.length }
|
|
145
|
+
|
|
146
|
+
// Write archived entries
|
|
147
|
+
let archiveContent = ""
|
|
148
|
+
if (existsSync(ARCHIVE_FILE)) {
|
|
149
|
+
archiveContent = readFileSync(ARCHIVE_FILE, "utf-8")
|
|
150
|
+
} else {
|
|
151
|
+
archiveContent = `version: 1\narchived:\n`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const archiveLines = archiveContent.split("\n")
|
|
155
|
+
let archiveHeaderIdx = archiveLines.findIndex((l) => l.startsWith("archived["))
|
|
156
|
+
if (archiveHeaderIdx === -1) {
|
|
157
|
+
archiveLines.push(`archived[0|]{id|category|key|content|file|tags|date}:`)
|
|
158
|
+
archiveHeaderIdx = archiveLines.length - 1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const archiveMatch = archiveLines[archiveHeaderIdx].match(/archived\[(\d+)\|/)
|
|
162
|
+
const archiveCount = archiveMatch ? parseInt(archiveMatch[1]) : 0
|
|
163
|
+
|
|
164
|
+
for (const entry of toArchive) {
|
|
165
|
+
archiveLines.splice(archiveHeaderIdx + 1, 0, ` ${entry}`)
|
|
166
|
+
}
|
|
167
|
+
archiveLines[archiveHeaderIdx] = archiveLines[archiveHeaderIdx].replace(/archived\[\d+\|/, `[${archiveCount + toArchive.length}|`)
|
|
168
|
+
|
|
169
|
+
writeFileSync(ARCHIVE_FILE, archiveLines.join("\n"))
|
|
170
|
+
|
|
171
|
+
// Update main file
|
|
172
|
+
lines[headerIdx] = lines[headerIdx].replace(/entries\[\d+\|/, `[${toKeep.length}|`)
|
|
173
|
+
lines.splice(headerIdx + 1, entryLines.length, ...toKeep.map((l) => ` ${l}`))
|
|
174
|
+
writeMemory(lines.join("\n"))
|
|
175
|
+
|
|
176
|
+
return { archived: toArchive.length, kept: toKeep.length }
|
|
177
|
+
}
|
|
178
|
+
|
|
34
179
|
const server = new McpServer(
|
|
35
180
|
{ name: "toon-memory", version: "1.1.0" },
|
|
36
181
|
{ capabilities: { tools: { listChanged: true } } }
|
|
@@ -251,5 +396,91 @@ server.registerTool(
|
|
|
251
396
|
}
|
|
252
397
|
)
|
|
253
398
|
|
|
399
|
+
server.registerTool(
|
|
400
|
+
"memory_archive",
|
|
401
|
+
{
|
|
402
|
+
title: "Archive Old Entries",
|
|
403
|
+
description: "Mover entradas antiguas (>30 días) a archive.toon para mantener la memoria limpia.",
|
|
404
|
+
inputSchema: {},
|
|
405
|
+
},
|
|
406
|
+
async () => {
|
|
407
|
+
const result = archiveOldEntries()
|
|
408
|
+
|
|
409
|
+
if (result.archived === 0) {
|
|
410
|
+
return { content: [{ type: "text" as const, text: "No hay entradas antiguas para archivar" }] }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
content: [{
|
|
415
|
+
type: "text" as const,
|
|
416
|
+
text: `📦 Archivadas ${result.archived} entradas antiguas\n📋 Quedan ${result.kept} entradas activas`
|
|
417
|
+
}],
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
)
|
|
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
|
+
|
|
254
485
|
const transport = new StdioServerTransport()
|
|
255
486
|
await server.connect(transport)
|