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/README.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# toon-memory
|
|
2
2
|
|
|
3
|
-
Persistent memory for AI coding agents — remember decisions, patterns, and bugs between sessions.
|
|
3
|
+
> Persistent memory for AI coding agents — remember decisions, patterns, and bugs between sessions.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/toon-memory)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://github.com/LuiggiVal08/toon-memory/actions/workflows/ci.yml)
|
|
8
|
+
[](https://luiggival08.github.io/toon-memory/)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [What is toon-memory?](#what-is-toon-memory)
|
|
15
|
+
- [Blog Post](#blog-post)
|
|
16
|
+
- [Features](#features)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Supported Agents](#supported-agents)
|
|
19
|
+
- [MCP Tools](#mcp-tools)
|
|
20
|
+
- [CLI Commands](#cli-commands)
|
|
21
|
+
- [Configuration](#configuration)
|
|
22
|
+
- [How It Works](#how-it-works)
|
|
23
|
+
- [Why TOON?](#why-toon)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [Contributing](#contributing)
|
|
26
|
+
- [License](#license)
|
|
8
27
|
|
|
9
28
|
---
|
|
10
29
|
|
|
@@ -12,6 +31,8 @@ Persistent memory for AI coding agents — remember decisions, patterns, and bug
|
|
|
12
31
|
|
|
13
32
|
AI agents forget everything between sessions. toon-memory fixes this by providing persistent memory that survives restarts.
|
|
14
33
|
|
|
34
|
+
📖 **[Read the documentation](https://luiggival08.github.io/toon-memory/)**
|
|
35
|
+
|
|
15
36
|
**Use cases:**
|
|
16
37
|
- Remember design decisions ("Why did we choose X over Y?")
|
|
17
38
|
- Track patterns ("This project uses Zod for validation")
|
|
@@ -20,16 +41,24 @@ AI agents forget everything between sessions. toon-memory fixes this by providin
|
|
|
20
41
|
|
|
21
42
|
---
|
|
22
43
|
|
|
44
|
+
## Blog Post
|
|
45
|
+
|
|
46
|
+
Read [How toon-memory Makes Your AI Agent Smarter](https://luiggival08.github.io/toon-memory/blog) to see a real-world demo of persistent memory in action.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
23
50
|
## Features
|
|
24
51
|
|
|
25
|
-
- **
|
|
52
|
+
- **8 MCP tools** — Full memory management via Model Context Protocol
|
|
26
53
|
- **7 agents supported** — OpenCode, VS Code/Copilot, Claude Code, Cursor, Windsurf, Cline, Continue
|
|
27
54
|
- **TOON format** — 40% fewer tokens than JSON, better LLM comprehension
|
|
28
|
-
- **Per-project memory** —
|
|
29
|
-
- **Zero config** —
|
|
30
|
-
- **Auto gitignore** —
|
|
31
|
-
- **Date filtering** —
|
|
32
|
-
- **Auto-archive** —
|
|
55
|
+
- **Per-project memory** — Each project gets its own memory file
|
|
56
|
+
- **Zero config** — Just install and use
|
|
57
|
+
- **Auto gitignore** — Automatically adds `.opencode/memory/` to `.gitignore`
|
|
58
|
+
- **Date filtering** — Search memory by date range
|
|
59
|
+
- **Auto-archive** — Old entries (>30 days) moved to archive automatically
|
|
60
|
+
- **Encryption** — AES-256-GCM encryption for sensitive data
|
|
61
|
+
- **Watch mode** — Auto-backup every N minutes
|
|
33
62
|
|
|
34
63
|
---
|
|
35
64
|
|
|
@@ -81,7 +110,7 @@ memory_remember # Save important decisions
|
|
|
81
110
|
|
|
82
111
|
---
|
|
83
112
|
|
|
84
|
-
## Tools
|
|
113
|
+
## MCP Tools
|
|
85
114
|
|
|
86
115
|
| Tool | Description |
|
|
87
116
|
|------|-------------|
|
|
@@ -91,13 +120,36 @@ memory_remember # Save important decisions
|
|
|
91
120
|
| `memory_stats` | View memory state |
|
|
92
121
|
| `memory_summary` | Save/retrieve file summaries |
|
|
93
122
|
| `memory_archive` | Archive old entries (>30 days) |
|
|
123
|
+
| `memory_encrypt` | Enable AES-256-GCM encryption |
|
|
124
|
+
| `memory_decrypt` | Disable encryption |
|
|
125
|
+
|
|
126
|
+
### Examples
|
|
94
127
|
|
|
95
|
-
|
|
128
|
+
#### Remember a decision
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
memory_remember({
|
|
132
|
+
category: "decision",
|
|
133
|
+
key: "use-zod",
|
|
134
|
+
content: "Use Zod for validation",
|
|
135
|
+
file: "src/types.ts",
|
|
136
|
+
tags: "validation;types"
|
|
137
|
+
})
|
|
138
|
+
// 🧠 Guardado: decision/use-zod (a1b2c3d4)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### Search memory
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
memory_recall({ query: "redis" })
|
|
145
|
+
// [bug] redis-pool-fix (i9j0k1l2)
|
|
146
|
+
// Added max_connections=20
|
|
147
|
+
// File: redis.ts | Tags: redis;fix | Date: 2026-07-10
|
|
148
|
+
```
|
|
96
149
|
|
|
97
|
-
Search
|
|
150
|
+
#### Search with date filter
|
|
98
151
|
|
|
99
152
|
```typescript
|
|
100
|
-
// Search with date filter
|
|
101
153
|
memory_recall({
|
|
102
154
|
query: "redis",
|
|
103
155
|
from_date: "2026-07-01",
|
|
@@ -105,36 +157,21 @@ memory_recall({
|
|
|
105
157
|
})
|
|
106
158
|
```
|
|
107
159
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Entries older than 30 days are automatically archived to keep memory clean:
|
|
160
|
+
#### Archive old entries
|
|
111
161
|
|
|
112
162
|
```typescript
|
|
113
|
-
// Manually trigger archiving
|
|
114
163
|
memory_archive()
|
|
115
164
|
// 📦 Archivadas 5 entradas antiguas
|
|
116
165
|
// 📋 Quedan 42 entradas activas
|
|
117
166
|
```
|
|
118
167
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## How It Works
|
|
122
|
-
|
|
123
|
-
1. **MCP Server** — runs locally, talks to your agent via stdio
|
|
124
|
-
2. **TOON Format** — stores data in Token-Oriented Object Notation (~40% fewer tokens than JSON)
|
|
125
|
-
3. **Per-project memory** — each project gets `.opencode/memory/data.toon`
|
|
126
|
-
4. **Zero config** — just install and use
|
|
168
|
+
#### Enable encryption
|
|
127
169
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
a1b2c3d4|decision|use-zod|Use Zod for validation|src/types.ts|validation;types|2026-07-10
|
|
134
|
-
e5f6g7h8|pattern|pydantic-configs|Project uses Pydantic v2|config.py|python;patterns|2026-07-10
|
|
135
|
-
i9j0k1l2|bug|redis-pool-fix|Added max_connections=20|redis.ts|redis;fix|2026-07-10
|
|
136
|
-
summaries:
|
|
137
|
-
src/services/redis.ts: Redis connection pool with retry logic
|
|
170
|
+
```typescript
|
|
171
|
+
memory_encrypt()
|
|
172
|
+
// 🔐 Encriptación habilitada
|
|
173
|
+
// ⚠️ Guarda esta clave (no se puede recuperar):
|
|
174
|
+
// a1b2c3d4...
|
|
138
175
|
```
|
|
139
176
|
|
|
140
177
|
---
|
|
@@ -149,11 +186,14 @@ npx toon-memory status # Check installation status
|
|
|
149
186
|
npx toon-memory stats # View memory statistics
|
|
150
187
|
npx toon-memory export # Export memory to JSON
|
|
151
188
|
npx toon-memory import <file> # Import memory from JSON
|
|
189
|
+
npx toon-memory watch [options] # Auto-backup with options
|
|
152
190
|
npx toon-memory upgrade # Update to latest version
|
|
153
191
|
npx toon-memory uninstall # Remove from all agents
|
|
154
192
|
```
|
|
155
193
|
|
|
156
|
-
###
|
|
194
|
+
### Examples
|
|
195
|
+
|
|
196
|
+
#### Stats
|
|
157
197
|
|
|
158
198
|
```bash
|
|
159
199
|
$ npx toon-memory stats
|
|
@@ -171,7 +211,7 @@ Last updated: 2026-07-10
|
|
|
171
211
|
File size: 12.4 KB
|
|
172
212
|
```
|
|
173
213
|
|
|
174
|
-
|
|
214
|
+
#### Export
|
|
175
215
|
|
|
176
216
|
```bash
|
|
177
217
|
$ npx toon-memory export
|
|
@@ -182,7 +222,7 @@ Exported 45 entries to:
|
|
|
182
222
|
/path/to/project/toon-memory-export.json
|
|
183
223
|
```
|
|
184
224
|
|
|
185
|
-
|
|
225
|
+
#### Import
|
|
186
226
|
|
|
187
227
|
```bash
|
|
188
228
|
$ npx toon-memory import backup.json
|
|
@@ -193,6 +233,34 @@ Imported 3 new entries
|
|
|
193
233
|
Skipped 2 duplicates
|
|
194
234
|
```
|
|
195
235
|
|
|
236
|
+
#### Watch
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
$ npx toon-memory watch 15 -c -m 20
|
|
240
|
+
|
|
241
|
+
🧠 toon-memory watch
|
|
242
|
+
|
|
243
|
+
Watching memory file every 15 minutes...
|
|
244
|
+
Max backups: 20
|
|
245
|
+
Compression: enabled
|
|
246
|
+
Logging: disabled
|
|
247
|
+
Press Ctrl+C to stop
|
|
248
|
+
|
|
249
|
+
📦 Backup #1 created: 2026-07-11T16-00-00-000Z
|
|
250
|
+
📦 Backup #2 created: 2026-07-11T16-15-00-000Z
|
|
251
|
+
^C
|
|
252
|
+
✅ Watch stopped. 2 backups created.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Watch Options:**
|
|
256
|
+
|
|
257
|
+
| Option | Description | Default |
|
|
258
|
+
|--------|-------------|---------|
|
|
259
|
+
| `[interval]` | Backup interval in minutes | 5 |
|
|
260
|
+
| `-c, --compress` | Enable gzip compression | off |
|
|
261
|
+
| `-l, --log [path]` | Enable file logging | off |
|
|
262
|
+
| `-m, --max-backups <n>` | Max backups to keep (0=unlimited) | 10 |
|
|
263
|
+
|
|
196
264
|
---
|
|
197
265
|
|
|
198
266
|
## Configuration
|
|
@@ -246,6 +314,41 @@ Add to `.vscode/mcp.json`:
|
|
|
246
314
|
|
|
247
315
|
---
|
|
248
316
|
|
|
317
|
+
## How It Works
|
|
318
|
+
|
|
319
|
+
1. **MCP Server** — Runs locally, talks to your agent via stdio
|
|
320
|
+
2. **TOON Format** — Stores data in Token-Oriented Object Notation (~40% fewer tokens than JSON)
|
|
321
|
+
3. **Per-project memory** — Each project gets `.opencode/memory/data.toon`
|
|
322
|
+
4. **Zero config** — Just install and use
|
|
323
|
+
|
|
324
|
+
### Memory File Format
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
version: 1
|
|
328
|
+
entries[3|]{id|category|key|content|file|tags|date}:
|
|
329
|
+
a1b2c3d4|decision|use-zod|Use Zod for validation|src/types.ts|validation;types|2026-07-10
|
|
330
|
+
e5f6g7h8|pattern|pydantic-configs|Project uses Pydantic v2|config.py|python;patterns|2026-07-10
|
|
331
|
+
i9j0k1l2|bug|redis-pool-fix|Added max_connections=20|redis.ts|redis;fix|2026-07-10
|
|
332
|
+
summaries:
|
|
333
|
+
src/services/redis.ts: Redis connection pool with retry logic
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### File Structure
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
.opencode/
|
|
340
|
+
├── memory/
|
|
341
|
+
│ ├── data.toon # Main memory file
|
|
342
|
+
│ ├── archive.toon # Archived entries (>30 days)
|
|
343
|
+
│ ├── config.json # Encryption settings
|
|
344
|
+
│ └── backups/ # Watch mode backups
|
|
345
|
+
│ ├── backup-2026-07-11T16-00-00-000Z.toon
|
|
346
|
+
│ └── backup-2026-07-11T16-10-00-000Z.toon
|
|
347
|
+
└── opencode.json # MCP server config
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
249
352
|
## Why TOON?
|
|
250
353
|
|
|
251
354
|
TOON (Token-Oriented Object Notation) is designed for LLMs:
|
|
@@ -257,8 +360,8 @@ TOON (Token-Oriented Object Notation) is designed for LLMs:
|
|
|
257
360
|
| **TOON** | **60** | **76.4%** |
|
|
258
361
|
|
|
259
362
|
- **40% fewer tokens** than JSON
|
|
260
|
-
- **Lossless roundtrip** —
|
|
261
|
-
- **Better LLM comprehension** —
|
|
363
|
+
- **Lossless roundtrip** — No data loss
|
|
364
|
+
- **Better LLM comprehension** — Structured for AI consumption
|
|
262
365
|
|
|
263
366
|
---
|
|
264
367
|
|
|
@@ -272,6 +375,30 @@ npm run build
|
|
|
272
375
|
npm test
|
|
273
376
|
```
|
|
274
377
|
|
|
378
|
+
### Project Structure
|
|
379
|
+
|
|
380
|
+
```
|
|
381
|
+
toon-memory/
|
|
382
|
+
├── src/
|
|
383
|
+
│ ├── bin/
|
|
384
|
+
│ │ └── toon-memory.ts # Entry point
|
|
385
|
+
│ ├── cli/
|
|
386
|
+
│ │ ├── setup.ts # CLI commands
|
|
387
|
+
│ │ └── toon-memory.ts # CLI runner
|
|
388
|
+
│ ├── mcp/
|
|
389
|
+
│ │ └── server.ts # MCP server (8 tools)
|
|
390
|
+
│ └── memory.ts # Custom tool (OpenCode)
|
|
391
|
+
├── tests/
|
|
392
|
+
│ ├── cli.test.ts # CLI tests
|
|
393
|
+
│ └── memory.test.ts # Memory tests
|
|
394
|
+
├── .github/workflows/
|
|
395
|
+
│ ├── ci.yml # CI (Node.js 20/22)
|
|
396
|
+
│ └── publish.yml # Auto-publish on release
|
|
397
|
+
├── package.json
|
|
398
|
+
├── tsconfig.json
|
|
399
|
+
└── vitest.config.ts
|
|
400
|
+
```
|
|
401
|
+
|
|
275
402
|
---
|
|
276
403
|
|
|
277
404
|
## Contributing
|
package/bin/toon-memory.js
CHANGED
|
File without changes
|
package/dist/cli/setup.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync } from "fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { basename, dirname, join } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { createInterface } from "readline";
|
|
6
6
|
import { createRequire } from "module";
|
|
7
|
+
import { gzipSync, gunzipSync } from "zlib";
|
|
7
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const projectRoot = process.cwd();
|
|
9
10
|
const sourceDir = join(__dirname, "..", "..", "src");
|
|
@@ -112,10 +113,18 @@ function installMCPConfig(agent, scope) {
|
|
|
112
113
|
}
|
|
113
114
|
const mcpKey = agent.mcpKey || "mcpServers";
|
|
114
115
|
if (!config[mcpKey]) config[mcpKey] = {};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
if (agent.name === "opencode") {
|
|
117
|
+
config[mcpKey]["toon-memory"] = {
|
|
118
|
+
enabled: true,
|
|
119
|
+
type: "local",
|
|
120
|
+
command: ["npx", "-y", "toon-memory", "mcp"]
|
|
121
|
+
};
|
|
122
|
+
} else {
|
|
123
|
+
config[mcpKey]["toon-memory"] = {
|
|
124
|
+
command: "npx",
|
|
125
|
+
args: ["-y", "toon-memory", "mcp"]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
119
128
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
120
129
|
console.log(` MCP server added to ${configPath}`);
|
|
121
130
|
}
|
|
@@ -335,6 +344,134 @@ ${newLines}
|
|
|
335
344
|
console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates
|
|
336
345
|
`);
|
|
337
346
|
}
|
|
347
|
+
function parseWatchOptions(args2) {
|
|
348
|
+
const opts = {
|
|
349
|
+
interval: 5,
|
|
350
|
+
maxBackups: 10,
|
|
351
|
+
compress: false,
|
|
352
|
+
logFile: false,
|
|
353
|
+
logPath: ""
|
|
354
|
+
};
|
|
355
|
+
for (let i = 1; i < args2.length; i++) {
|
|
356
|
+
const arg = args2[i];
|
|
357
|
+
if (arg === "--compress" || arg === "-c") {
|
|
358
|
+
opts.compress = true;
|
|
359
|
+
} else if (arg === "--log" || arg === "-l") {
|
|
360
|
+
opts.logFile = true;
|
|
361
|
+
opts.logPath = args2[++i] || join(projectRoot, ".opencode", "memory", "watch.log");
|
|
362
|
+
} else if (arg === "--max-backups" || arg === "-m") {
|
|
363
|
+
opts.maxBackups = parseInt(args2[++i]) || 10;
|
|
364
|
+
} else if (!arg.startsWith("-")) {
|
|
365
|
+
opts.interval = parseInt(arg) || 5;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return opts;
|
|
369
|
+
}
|
|
370
|
+
function writeWatchLog(logPath, message) {
|
|
371
|
+
if (!logPath) return;
|
|
372
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
373
|
+
const logLine = `[${timestamp}] ${message}
|
|
374
|
+
`;
|
|
375
|
+
writeFileSync(logPath, logLine, { flag: "a" });
|
|
376
|
+
}
|
|
377
|
+
function getBackupFiles(backupDir) {
|
|
378
|
+
if (!existsSync(backupDir)) return [];
|
|
379
|
+
return readdirSync(backupDir).filter((f) => f.startsWith("backup-") && (f.endsWith(".toon") || f.endsWith(".gz"))).map((f) => join(backupDir, f)).sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs);
|
|
380
|
+
}
|
|
381
|
+
function pruneBackups(backupDir, maxBackups) {
|
|
382
|
+
if (maxBackups <= 0) return 0;
|
|
383
|
+
const files = getBackupFiles(backupDir);
|
|
384
|
+
const excess = files.length - maxBackups;
|
|
385
|
+
if (excess <= 0) return 0;
|
|
386
|
+
for (let i = 0; i < excess; i++) {
|
|
387
|
+
unlinkSync(files[i]);
|
|
388
|
+
}
|
|
389
|
+
return excess;
|
|
390
|
+
}
|
|
391
|
+
function compressData(data) {
|
|
392
|
+
return gzipSync(Buffer.from(data, "utf-8"));
|
|
393
|
+
}
|
|
394
|
+
function decompressData(data) {
|
|
395
|
+
return gunzipSync(data).toString("utf-8");
|
|
396
|
+
}
|
|
397
|
+
function watch() {
|
|
398
|
+
console.log("\n\u{1F9E0} toon-memory watch\n");
|
|
399
|
+
const memoryFile = join(projectRoot, ".opencode", "memory", "data.toon");
|
|
400
|
+
const backupDir = join(projectRoot, ".opencode", "memory", "backups");
|
|
401
|
+
if (!existsSync(memoryFile)) {
|
|
402
|
+
console.log("Memory not initialized. Run 'npx toon-memory init' first.\n");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });
|
|
406
|
+
const opts = parseWatchOptions(args);
|
|
407
|
+
if (opts.logFile) {
|
|
408
|
+
const logDir = dirname(opts.logPath);
|
|
409
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
410
|
+
}
|
|
411
|
+
console.log(`Watching memory file every ${opts.interval} minutes...`);
|
|
412
|
+
console.log(`Max backups: ${opts.maxBackups === 0 ? "unlimited" : opts.maxBackups}`);
|
|
413
|
+
console.log(`Compression: ${opts.compress ? "enabled" : "disabled"}`);
|
|
414
|
+
console.log(`Logging: ${opts.logFile ? `enabled (${opts.logPath})` : "disabled"}`);
|
|
415
|
+
console.log(`Press Ctrl+C to stop
|
|
416
|
+
`);
|
|
417
|
+
let lastContent = readFileSync(memoryFile, "utf-8");
|
|
418
|
+
let lastHash = hashContent(lastContent);
|
|
419
|
+
let backupCount = 0;
|
|
420
|
+
if (opts.logFile) writeWatchLog(opts.logPath, "Watch started");
|
|
421
|
+
const backup = () => {
|
|
422
|
+
try {
|
|
423
|
+
const currentContent = readFileSync(memoryFile, "utf-8");
|
|
424
|
+
const currentHash = hashContent(currentContent);
|
|
425
|
+
if (currentHash !== lastHash) {
|
|
426
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
427
|
+
const ext = opts.compress ? ".toon.gz" : ".toon";
|
|
428
|
+
const backupFile = join(backupDir, `backup-${timestamp}${ext}`);
|
|
429
|
+
if (opts.compress) {
|
|
430
|
+
writeFileSync(backupFile, compressData(currentContent));
|
|
431
|
+
} else {
|
|
432
|
+
writeFileSync(backupFile, currentContent);
|
|
433
|
+
}
|
|
434
|
+
backupCount++;
|
|
435
|
+
console.log(`\u{1F4E6} Backup #${backupCount} created: ${timestamp}`);
|
|
436
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Backup #${backupCount}: ${timestamp}`);
|
|
437
|
+
lastContent = currentContent;
|
|
438
|
+
lastHash = currentHash;
|
|
439
|
+
const pruned = pruneBackups(backupDir, opts.maxBackups);
|
|
440
|
+
if (pruned > 0) {
|
|
441
|
+
console.log(`\u{1F5D1}\uFE0F Pruned ${pruned} old backup(s)`);
|
|
442
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Pruned ${pruned} old backup(s)`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const msg = `Error creating backup: ${err.message}`;
|
|
447
|
+
console.error(`\u274C ${msg}`);
|
|
448
|
+
if (opts.logFile) writeWatchLog(opts.logPath, msg);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
function hashContent(content) {
|
|
452
|
+
let hash = 0;
|
|
453
|
+
for (let i = 0; i < content.length; i++) {
|
|
454
|
+
const char = content.charCodeAt(i);
|
|
455
|
+
hash = (hash << 5) - hash + char;
|
|
456
|
+
hash = hash & hash;
|
|
457
|
+
}
|
|
458
|
+
return hash.toString(36);
|
|
459
|
+
}
|
|
460
|
+
backup();
|
|
461
|
+
const interval = setInterval(backup, opts.interval * 60 * 1e3);
|
|
462
|
+
process.on("SIGINT", () => {
|
|
463
|
+
clearInterval(interval);
|
|
464
|
+
console.log(`
|
|
465
|
+
\u2705 Watch stopped. ${backupCount} backups created.
|
|
466
|
+
`);
|
|
467
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Watch stopped. ${backupCount} backups created.`);
|
|
468
|
+
process.exit(0);
|
|
469
|
+
});
|
|
470
|
+
process.on("SIGTERM", () => {
|
|
471
|
+
clearInterval(interval);
|
|
472
|
+
process.exit(0);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
338
475
|
const args = process.argv.slice(2);
|
|
339
476
|
if (args[0] === "uninstall") {
|
|
340
477
|
uninstall();
|
|
@@ -364,6 +501,10 @@ if (args[0] === "import") {
|
|
|
364
501
|
importMemory();
|
|
365
502
|
process.exit(0);
|
|
366
503
|
}
|
|
504
|
+
if (args[0] === "watch") {
|
|
505
|
+
watch();
|
|
506
|
+
process.exit(0);
|
|
507
|
+
}
|
|
367
508
|
const agents = detectAgents();
|
|
368
509
|
console.log("\n\u{1F9E0} toon-memory installer\n");
|
|
369
510
|
console.log("Supported agents:");
|
package/mcp/server.js
CHANGED
|
@@ -27642,25 +27642,82 @@ 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");
|
|
27650
27651
|
var ARCHIVE_DAYS = 30;
|
|
27651
|
-
|
|
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() {
|
|
27652
27668
|
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true });
|
|
27669
|
+
}
|
|
27670
|
+
function ensureMemoryFile() {
|
|
27671
|
+
ensureMemoryDir();
|
|
27653
27672
|
if (!existsSync(MEMORY_FILE)) {
|
|
27654
27673
|
writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n");
|
|
27655
27674
|
}
|
|
27656
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
|
+
}
|
|
27657
27699
|
function readMemory() {
|
|
27658
27700
|
ensureMemoryFile();
|
|
27659
|
-
|
|
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;
|
|
27660
27711
|
}
|
|
27661
27712
|
function writeMemory(content) {
|
|
27662
27713
|
ensureMemoryFile();
|
|
27663
|
-
|
|
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
|
+
}
|
|
27664
27721
|
}
|
|
27665
27722
|
function generateId() {
|
|
27666
27723
|
return randomBytes(4).toString("hex");
|
|
@@ -27928,6 +27985,60 @@ server.registerTool(
|
|
|
27928
27985
|
};
|
|
27929
27986
|
}
|
|
27930
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
|
+
);
|
|
27931
28042
|
var transport = new StdioServerTransport();
|
|
27932
28043
|
await server.connect(transport);
|
|
27933
28044
|
/*! Bundled license information:
|