toon-memory 1.5.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 +157 -51
- package/bin/toon-memory.js +0 -0
- package/dist/cli/setup.js +113 -16
- package/package.json +1 -1
- package/src/cli/setup.ts +305 -28
- package/src/mcp/server.ts +193 -2
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,18 +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
|
-
- **8 MCP tools** —
|
|
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
|
|
33
60
|
- **Encryption** — AES-256-GCM encryption for sensitive data
|
|
34
|
-
- **Watch mode** —
|
|
61
|
+
- **Watch mode** — Auto-backup every N minutes
|
|
35
62
|
|
|
36
63
|
---
|
|
37
64
|
|
|
@@ -83,7 +110,7 @@ memory_remember # Save important decisions
|
|
|
83
110
|
|
|
84
111
|
---
|
|
85
112
|
|
|
86
|
-
## Tools
|
|
113
|
+
## MCP Tools
|
|
87
114
|
|
|
88
115
|
| Tool | Description |
|
|
89
116
|
|------|-------------|
|
|
@@ -96,12 +123,33 @@ memory_remember # Save important decisions
|
|
|
96
123
|
| `memory_encrypt` | Enable AES-256-GCM encryption |
|
|
97
124
|
| `memory_decrypt` | Disable encryption |
|
|
98
125
|
|
|
99
|
-
###
|
|
126
|
+
### Examples
|
|
127
|
+
|
|
128
|
+
#### Remember a decision
|
|
100
129
|
|
|
101
|
-
|
|
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
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Search with date filter
|
|
102
151
|
|
|
103
152
|
```typescript
|
|
104
|
-
// Search with date filter
|
|
105
153
|
memory_recall({
|
|
106
154
|
query: "redis",
|
|
107
155
|
from_date: "2026-07-01",
|
|
@@ -109,52 +157,21 @@ memory_recall({
|
|
|
109
157
|
})
|
|
110
158
|
```
|
|
111
159
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
Entries older than 30 days are automatically archived to keep memory clean:
|
|
160
|
+
#### Archive old entries
|
|
115
161
|
|
|
116
162
|
```typescript
|
|
117
|
-
// Manually trigger archiving
|
|
118
163
|
memory_archive()
|
|
119
164
|
// 📦 Archivadas 5 entradas antiguas
|
|
120
165
|
// 📋 Quedan 42 entradas activas
|
|
121
166
|
```
|
|
122
167
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Enable AES-256-GCM encryption for sensitive data:
|
|
168
|
+
#### Enable encryption
|
|
126
169
|
|
|
127
170
|
```typescript
|
|
128
|
-
// Enable encryption
|
|
129
171
|
memory_encrypt()
|
|
130
172
|
// 🔐 Encriptación habilitada
|
|
131
173
|
// ⚠️ Guarda esta clave (no se puede recuperar):
|
|
132
174
|
// a1b2c3d4...
|
|
133
|
-
|
|
134
|
-
// Disable encryption
|
|
135
|
-
memory_decrypt({ key: "a1b2c3d4..." })
|
|
136
|
-
// 🔓 Encriptación deshabilitada
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## How It Works
|
|
142
|
-
|
|
143
|
-
1. **MCP Server** — runs locally, talks to your agent via stdio
|
|
144
|
-
2. **TOON Format** — stores data in Token-Oriented Object Notation (~40% fewer tokens than JSON)
|
|
145
|
-
3. **Per-project memory** — each project gets `.opencode/memory/data.toon`
|
|
146
|
-
4. **Zero config** — just install and use
|
|
147
|
-
|
|
148
|
-
### Memory File Format
|
|
149
|
-
|
|
150
|
-
```
|
|
151
|
-
version: 1
|
|
152
|
-
entries[3|]{id|category|key|content|file|tags|date}:
|
|
153
|
-
a1b2c3d4|decision|use-zod|Use Zod for validation|src/types.ts|validation;types|2026-07-10
|
|
154
|
-
e5f6g7h8|pattern|pydantic-configs|Project uses Pydantic v2|config.py|python;patterns|2026-07-10
|
|
155
|
-
i9j0k1l2|bug|redis-pool-fix|Added max_connections=20|redis.ts|redis;fix|2026-07-10
|
|
156
|
-
summaries:
|
|
157
|
-
src/services/redis.ts: Redis connection pool with retry logic
|
|
158
175
|
```
|
|
159
176
|
|
|
160
177
|
---
|
|
@@ -169,12 +186,14 @@ npx toon-memory status # Check installation status
|
|
|
169
186
|
npx toon-memory stats # View memory statistics
|
|
170
187
|
npx toon-memory export # Export memory to JSON
|
|
171
188
|
npx toon-memory import <file> # Import memory from JSON
|
|
172
|
-
npx toon-memory watch [
|
|
189
|
+
npx toon-memory watch [options] # Auto-backup with options
|
|
173
190
|
npx toon-memory upgrade # Update to latest version
|
|
174
191
|
npx toon-memory uninstall # Remove from all agents
|
|
175
192
|
```
|
|
176
193
|
|
|
177
|
-
###
|
|
194
|
+
### Examples
|
|
195
|
+
|
|
196
|
+
#### Stats
|
|
178
197
|
|
|
179
198
|
```bash
|
|
180
199
|
$ npx toon-memory stats
|
|
@@ -192,7 +211,7 @@ Last updated: 2026-07-10
|
|
|
192
211
|
File size: 12.4 KB
|
|
193
212
|
```
|
|
194
213
|
|
|
195
|
-
|
|
214
|
+
#### Export
|
|
196
215
|
|
|
197
216
|
```bash
|
|
198
217
|
$ npx toon-memory export
|
|
@@ -203,7 +222,7 @@ Exported 45 entries to:
|
|
|
203
222
|
/path/to/project/toon-memory-export.json
|
|
204
223
|
```
|
|
205
224
|
|
|
206
|
-
|
|
225
|
+
#### Import
|
|
207
226
|
|
|
208
227
|
```bash
|
|
209
228
|
$ npx toon-memory import backup.json
|
|
@@ -214,6 +233,34 @@ Imported 3 new entries
|
|
|
214
233
|
Skipped 2 duplicates
|
|
215
234
|
```
|
|
216
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
|
+
|
|
217
264
|
---
|
|
218
265
|
|
|
219
266
|
## Configuration
|
|
@@ -267,6 +314,41 @@ Add to `.vscode/mcp.json`:
|
|
|
267
314
|
|
|
268
315
|
---
|
|
269
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
|
+
|
|
270
352
|
## Why TOON?
|
|
271
353
|
|
|
272
354
|
TOON (Token-Oriented Object Notation) is designed for LLMs:
|
|
@@ -278,8 +360,8 @@ TOON (Token-Oriented Object Notation) is designed for LLMs:
|
|
|
278
360
|
| **TOON** | **60** | **76.4%** |
|
|
279
361
|
|
|
280
362
|
- **40% fewer tokens** than JSON
|
|
281
|
-
- **Lossless roundtrip** —
|
|
282
|
-
- **Better LLM comprehension** —
|
|
363
|
+
- **Lossless roundtrip** — No data loss
|
|
364
|
+
- **Better LLM comprehension** — Structured for AI consumption
|
|
283
365
|
|
|
284
366
|
---
|
|
285
367
|
|
|
@@ -293,6 +375,30 @@ npm run build
|
|
|
293
375
|
npm test
|
|
294
376
|
```
|
|
295
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
|
+
|
|
296
402
|
---
|
|
297
403
|
|
|
298
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,56 @@ ${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
|
+
}
|
|
338
397
|
function watch() {
|
|
339
398
|
console.log("\n\u{1F9E0} toon-memory watch\n");
|
|
340
399
|
const memoryFile = join(projectRoot, ".opencode", "memory", "data.toon");
|
|
@@ -344,30 +403,68 @@ function watch() {
|
|
|
344
403
|
return;
|
|
345
404
|
}
|
|
346
405
|
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });
|
|
347
|
-
const
|
|
348
|
-
|
|
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"}`);
|
|
349
415
|
console.log(`Press Ctrl+C to stop
|
|
350
416
|
`);
|
|
351
417
|
let lastContent = readFileSync(memoryFile, "utf-8");
|
|
418
|
+
let lastHash = hashContent(lastContent);
|
|
352
419
|
let backupCount = 0;
|
|
420
|
+
if (opts.logFile) writeWatchLog(opts.logPath, "Watch started");
|
|
353
421
|
const backup = () => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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);
|
|
362
449
|
}
|
|
363
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
|
+
}
|
|
364
460
|
backup();
|
|
365
|
-
const interval = setInterval(backup,
|
|
461
|
+
const interval = setInterval(backup, opts.interval * 60 * 1e3);
|
|
366
462
|
process.on("SIGINT", () => {
|
|
367
463
|
clearInterval(interval);
|
|
368
464
|
console.log(`
|
|
369
465
|
\u2705 Watch stopped. ${backupCount} backups created.
|
|
370
466
|
`);
|
|
467
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Watch stopped. ${backupCount} backups created.`);
|
|
371
468
|
process.exit(0);
|
|
372
469
|
});
|
|
373
470
|
process.on("SIGTERM", () => {
|
package/package.json
CHANGED
package/src/cli/setup.ts
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
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
10
|
const projectRoot = process.cwd()
|
|
@@ -12,10 +13,15 @@ const projectRoot = process.cwd()
|
|
|
12
13
|
const sourceDir = join(__dirname, "..", "..", "src")
|
|
13
14
|
const HOME = process.env.HOME || process.env.USERPROFILE || "~"
|
|
14
15
|
|
|
16
|
+
/** Supported AI coding agent configuration */
|
|
15
17
|
interface Agent {
|
|
18
|
+
/** Agent identifier (e.g., "opencode", "vscode/copilot") */
|
|
16
19
|
name: string
|
|
20
|
+
/** Global config file path (e.g., ~/.config/opencode/opencode.json) */
|
|
17
21
|
global?: string
|
|
22
|
+
/** Local (project-level) config file path (e.g., .opencode/opencode.json) */
|
|
18
23
|
local?: string
|
|
24
|
+
/** JSON key where MCP servers are stored */
|
|
19
25
|
mcpKey: string
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -27,7 +33,22 @@ try {
|
|
|
27
33
|
execSync("npm install @toon-format/toon", { cwd: projectRoot, stdio: "inherit" })
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Detect all supported AI coding agents on the system.
|
|
38
|
+
*
|
|
39
|
+
* Scans for configuration files in both global (~/.config/) and local
|
|
40
|
+
* (.opencode/, .vscode/, etc.) locations.
|
|
41
|
+
*
|
|
42
|
+
* @returns Array of detected agent configurations
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const agents = detectAgents()
|
|
47
|
+
* for (const agent of agents) {
|
|
48
|
+
* console.log(`${agent.name}: ${agent.local || "not found"}`)
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
31
52
|
function detectAgents(): Agent[] {
|
|
32
53
|
const agents: Agent[] = []
|
|
33
54
|
|
|
@@ -94,7 +115,17 @@ function detectAgents(): Agent[] {
|
|
|
94
115
|
return agents
|
|
95
116
|
}
|
|
96
117
|
|
|
97
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Install custom tools and memory directory for OpenCode.
|
|
120
|
+
*
|
|
121
|
+
* Creates `.opencode/tools/` and `.opencode/memory/` directories,
|
|
122
|
+
* copies `memory.ts` tool, and creates initial `data.toon` if needed.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```bash
|
|
126
|
+
* npx toon-memory init # Calls installOpenCodeTools()
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
98
129
|
function installOpenCodeTools(): void {
|
|
99
130
|
const toolsDir = join(projectRoot, ".opencode", "tools")
|
|
100
131
|
const memoryDir = join(projectRoot, ".opencode", "memory")
|
|
@@ -112,7 +143,17 @@ function installOpenCodeTools(): void {
|
|
|
112
143
|
}
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Add `.opencode/memory/` to `.gitignore` if not already present.
|
|
148
|
+
*
|
|
149
|
+
* Creates `.gitignore` if it doesn't exist, or appends the memory
|
|
150
|
+
* exclusion pattern to the existing file.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* ensureGitignore() // Adds .opencode/memory/ to .gitignore
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
116
157
|
function ensureGitignore(): void {
|
|
117
158
|
const gitignorePath = join(projectRoot, ".gitignore")
|
|
118
159
|
const entry = ".opencode/memory/"
|
|
@@ -130,7 +171,20 @@ function ensureGitignore(): void {
|
|
|
130
171
|
}
|
|
131
172
|
}
|
|
132
173
|
|
|
133
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Install MCP server configuration for an agent.
|
|
176
|
+
*
|
|
177
|
+
* Adds the `toon-memory` MCP server entry to the agent's config file.
|
|
178
|
+
*
|
|
179
|
+
* @param agent - Agent configuration with config path and MCP key
|
|
180
|
+
* @param scope - "global" or "local" installation scope
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const agent = { name: "opencode", local: ".opencode/opencode.json", mcpKey: "mcp" }
|
|
185
|
+
* installMCPConfig(agent, "local")
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
134
188
|
function installMCPConfig(agent: Agent, scope: string): void {
|
|
135
189
|
const configPath = scope === "global" ? agent.global : agent.local
|
|
136
190
|
|
|
@@ -154,16 +208,34 @@ function installMCPConfig(agent: Agent, scope: string): void {
|
|
|
154
208
|
const mcpKey = agent.mcpKey || "mcpServers"
|
|
155
209
|
if (!config[mcpKey]) config[mcpKey] = {}
|
|
156
210
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
// OpenCode uses a different format than other agents
|
|
212
|
+
if (agent.name === "opencode") {
|
|
213
|
+
config[mcpKey]["toon-memory"] = {
|
|
214
|
+
enabled: true,
|
|
215
|
+
type: "local",
|
|
216
|
+
command: ["npx", "-y", "toon-memory", "mcp"]
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
config[mcpKey]["toon-memory"] = {
|
|
220
|
+
command: "npx",
|
|
221
|
+
args: ["-y", "toon-memory", "mcp"]
|
|
222
|
+
}
|
|
160
223
|
}
|
|
161
224
|
|
|
162
225
|
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
163
226
|
console.log(` MCP server added to ${configPath}`)
|
|
164
227
|
}
|
|
165
228
|
|
|
166
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Uninstall toon-memory from all detected agents.
|
|
231
|
+
*
|
|
232
|
+
* Removes MCP server configurations and custom tools from all agents.
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* ```bash
|
|
236
|
+
* npx toon-memory uninstall
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
167
239
|
function uninstall(): void {
|
|
168
240
|
console.log("\n🧠 toon-memory uninstaller\n")
|
|
169
241
|
|
|
@@ -198,7 +270,20 @@ function uninstall(): void {
|
|
|
198
270
|
console.log("\n✅ toon-memory uninstalled from all agents\n")
|
|
199
271
|
}
|
|
200
272
|
|
|
201
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Initialize toon-memory for all detected agents (non-interactive).
|
|
275
|
+
*
|
|
276
|
+
* Installs MCP server configs, creates memory directory, and
|
|
277
|
+
* updates `.gitignore`.
|
|
278
|
+
*
|
|
279
|
+
* @param scope - "local" (default) or "global" installation scope
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```bash
|
|
283
|
+
* npx toon-memory init # Local install
|
|
284
|
+
* npx toon-memory init global # Global install
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
202
287
|
function init(scope: string = "local"): void {
|
|
203
288
|
console.log("\n🧠 toon-memory init\n")
|
|
204
289
|
|
|
@@ -220,7 +305,16 @@ function init(scope: string = "local"): void {
|
|
|
220
305
|
console.log("Done! Restart your agent to use memory tools.\n")
|
|
221
306
|
}
|
|
222
307
|
|
|
223
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Show toon-memory installation status.
|
|
310
|
+
*
|
|
311
|
+
* Displays version, memory entry count, and agent configuration status.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```bash
|
|
315
|
+
* npx toon-memory status
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
224
318
|
function status(): void {
|
|
225
319
|
console.log("\n🧠 toon-memory status\n")
|
|
226
320
|
|
|
@@ -272,7 +366,16 @@ function status(): void {
|
|
|
272
366
|
console.log("")
|
|
273
367
|
}
|
|
274
368
|
|
|
275
|
-
|
|
369
|
+
/**
|
|
370
|
+
* Upgrade toon-memory to the latest version.
|
|
371
|
+
*
|
|
372
|
+
* Checks npm registry for updates and installs if available.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```bash
|
|
376
|
+
* npx toon-memory upgrade
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
276
379
|
function upgrade(): void {
|
|
277
380
|
console.log("\n🧠 toon-memory upgrade\n")
|
|
278
381
|
|
|
@@ -291,7 +394,16 @@ function upgrade(): void {
|
|
|
291
394
|
}
|
|
292
395
|
}
|
|
293
396
|
|
|
294
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Display memory statistics.
|
|
399
|
+
*
|
|
400
|
+
* Shows entry counts by category, last update date, and file size.
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```bash
|
|
404
|
+
* npx toon-memory stats
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
295
407
|
function stats(): void {
|
|
296
408
|
console.log("\n🧠 toon-memory stats\n")
|
|
297
409
|
|
|
@@ -333,7 +445,17 @@ function stats(): void {
|
|
|
333
445
|
console.log("")
|
|
334
446
|
}
|
|
335
447
|
|
|
336
|
-
|
|
448
|
+
/**
|
|
449
|
+
* Export memory to JSON format.
|
|
450
|
+
*
|
|
451
|
+
* Creates a `toon-memory-export.json` file with all entries
|
|
452
|
+
* for backup or transfer to another project.
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```bash
|
|
456
|
+
* npx toon-memory export
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
337
459
|
function exportMemory(): void {
|
|
338
460
|
console.log("\n🧠 toon-memory export\n")
|
|
339
461
|
|
|
@@ -374,7 +496,18 @@ function exportMemory(): void {
|
|
|
374
496
|
console.log(` ${outputPath}\n`)
|
|
375
497
|
}
|
|
376
498
|
|
|
377
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Import memory from JSON file.
|
|
501
|
+
*
|
|
502
|
+
* Imports entries from a JSON export, skipping duplicates based on key.
|
|
503
|
+
*
|
|
504
|
+
* @param file - Path to JSON file (relative or absolute)
|
|
505
|
+
*
|
|
506
|
+
* @example
|
|
507
|
+
* ```bash
|
|
508
|
+
* npx toon-memory import toon-memory-export.json
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
378
511
|
function importMemory(): void {
|
|
379
512
|
console.log("\n🧠 toon-memory import\n")
|
|
380
513
|
|
|
@@ -446,7 +579,104 @@ function importMemory(): void {
|
|
|
446
579
|
console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates\n`)
|
|
447
580
|
}
|
|
448
581
|
|
|
449
|
-
|
|
582
|
+
/** Watch mode options */
|
|
583
|
+
interface WatchOptions {
|
|
584
|
+
/** Backup interval in minutes (default: 5) */
|
|
585
|
+
interval: number
|
|
586
|
+
/** Maximum number of backups to keep (0 = unlimited) */
|
|
587
|
+
maxBackups: number
|
|
588
|
+
/** Enable gzip compression for backups */
|
|
589
|
+
compress: boolean
|
|
590
|
+
/** Enable file logging */
|
|
591
|
+
logFile: boolean
|
|
592
|
+
/** Log file path */
|
|
593
|
+
logPath: string
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Parse watch CLI arguments into WatchOptions */
|
|
597
|
+
function parseWatchOptions(args: string[]): WatchOptions {
|
|
598
|
+
const opts: WatchOptions = {
|
|
599
|
+
interval: 5,
|
|
600
|
+
maxBackups: 10,
|
|
601
|
+
compress: false,
|
|
602
|
+
logFile: false,
|
|
603
|
+
logPath: ""
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
for (let i = 1; i < args.length; i++) {
|
|
607
|
+
const arg = args[i]
|
|
608
|
+
if (arg === "--compress" || arg === "-c") {
|
|
609
|
+
opts.compress = true
|
|
610
|
+
} else if (arg === "--log" || arg === "-l") {
|
|
611
|
+
opts.logFile = true
|
|
612
|
+
opts.logPath = args[++i] || join(projectRoot, ".opencode", "memory", "watch.log")
|
|
613
|
+
} else if (arg === "--max-backups" || arg === "-m") {
|
|
614
|
+
opts.maxBackups = parseInt(args[++i]) || 10
|
|
615
|
+
} else if (!arg.startsWith("-")) {
|
|
616
|
+
opts.interval = parseInt(arg) || 5
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return opts
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Write a line to the watch log file */
|
|
624
|
+
function writeWatchLog(logPath: string, message: string): void {
|
|
625
|
+
if (!logPath) return
|
|
626
|
+
const timestamp = new Date().toISOString()
|
|
627
|
+
const logLine = `[${timestamp}] ${message}\n`
|
|
628
|
+
writeFileSync(logPath, logLine, { flag: "a" })
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Get list of backup files sorted by creation time (oldest first) */
|
|
632
|
+
function getBackupFiles(backupDir: string): string[] {
|
|
633
|
+
if (!existsSync(backupDir)) return []
|
|
634
|
+
|
|
635
|
+
return readdirSync(backupDir)
|
|
636
|
+
.filter(f => f.startsWith("backup-") && (f.endsWith(".toon") || f.endsWith(".gz")))
|
|
637
|
+
.map(f => join(backupDir, f))
|
|
638
|
+
.sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Remove oldest backups if we exceed maxBackups */
|
|
642
|
+
function pruneBackups(backupDir: string, maxBackups: number): number {
|
|
643
|
+
if (maxBackups <= 0) return 0
|
|
644
|
+
|
|
645
|
+
const files = getBackupFiles(backupDir)
|
|
646
|
+
const excess = files.length - maxBackups
|
|
647
|
+
|
|
648
|
+
if (excess <= 0) return 0
|
|
649
|
+
|
|
650
|
+
for (let i = 0; i < excess; i++) {
|
|
651
|
+
unlinkSync(files[i])
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return excess
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** Compress content with gzip */
|
|
658
|
+
function compressData(data: string): Buffer {
|
|
659
|
+
return gzipSync(Buffer.from(data, "utf-8"))
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Decompress gzip data to string */
|
|
663
|
+
function decompressData(data: Buffer): string {
|
|
664
|
+
return gunzipSync(data).toString("utf-8")
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Watch mode - backup memory every N minutes
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```bash
|
|
672
|
+
* npx toon-memory watch # Default: 5 min interval, 10 max backups
|
|
673
|
+
* npx toon-memory watch 10 # 10 minute interval
|
|
674
|
+
* npx toon-memory watch -c # Enable compression
|
|
675
|
+
* npx toon-memory watch -l # Enable file logging
|
|
676
|
+
* npx toon-memory watch -m 20 # Keep max 20 backups
|
|
677
|
+
* npx toon-memory watch 15 -c -l -m 5 # All options
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
450
680
|
function watch(): void {
|
|
451
681
|
console.log("\n🧠 toon-memory watch\n")
|
|
452
682
|
|
|
@@ -460,36 +690,83 @@ function watch(): void {
|
|
|
460
690
|
|
|
461
691
|
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true })
|
|
462
692
|
|
|
463
|
-
const
|
|
464
|
-
|
|
693
|
+
const opts = parseWatchOptions(args)
|
|
694
|
+
|
|
695
|
+
if (opts.logFile) {
|
|
696
|
+
const logDir = dirname(opts.logPath)
|
|
697
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
console.log(`Watching memory file every ${opts.interval} minutes...`)
|
|
701
|
+
console.log(`Max backups: ${opts.maxBackups === 0 ? "unlimited" : opts.maxBackups}`)
|
|
702
|
+
console.log(`Compression: ${opts.compress ? "enabled" : "disabled"}`)
|
|
703
|
+
console.log(`Logging: ${opts.logFile ? `enabled (${opts.logPath})` : "disabled"}`)
|
|
465
704
|
console.log(`Press Ctrl+C to stop\n`)
|
|
466
705
|
|
|
467
706
|
let lastContent = readFileSync(memoryFile, "utf-8")
|
|
707
|
+
let lastHash = hashContent(lastContent)
|
|
468
708
|
let backupCount = 0
|
|
469
709
|
|
|
710
|
+
if (opts.logFile) writeWatchLog(opts.logPath, "Watch started")
|
|
711
|
+
|
|
470
712
|
const backup = () => {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
713
|
+
try {
|
|
714
|
+
const currentContent = readFileSync(memoryFile, "utf-8")
|
|
715
|
+
const currentHash = hashContent(currentContent)
|
|
716
|
+
|
|
717
|
+
if (currentHash !== lastHash) {
|
|
718
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
719
|
+
const ext = opts.compress ? ".toon.gz" : ".toon"
|
|
720
|
+
const backupFile = join(backupDir, `backup-${timestamp}${ext}`)
|
|
721
|
+
|
|
722
|
+
if (opts.compress) {
|
|
723
|
+
writeFileSync(backupFile, compressData(currentContent))
|
|
724
|
+
} else {
|
|
725
|
+
writeFileSync(backupFile, currentContent)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
backupCount++
|
|
729
|
+
console.log(`📦 Backup #${backupCount} created: ${timestamp}`)
|
|
730
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Backup #${backupCount}: ${timestamp}`)
|
|
731
|
+
|
|
732
|
+
lastContent = currentContent
|
|
733
|
+
lastHash = currentHash
|
|
734
|
+
|
|
735
|
+
const pruned = pruneBackups(backupDir, opts.maxBackups)
|
|
736
|
+
if (pruned > 0) {
|
|
737
|
+
console.log(`🗑️ Pruned ${pruned} old backup(s)`)
|
|
738
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Pruned ${pruned} old backup(s)`)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} catch (err) {
|
|
742
|
+
const msg = `Error creating backup: ${(err as Error).message}`
|
|
743
|
+
console.error(`❌ ${msg}`)
|
|
744
|
+
if (opts.logFile) writeWatchLog(opts.logPath, msg)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/** Simple hash for change detection (not cryptographic) */
|
|
749
|
+
function hashContent(content: string): string {
|
|
750
|
+
let hash = 0
|
|
751
|
+
for (let i = 0; i < content.length; i++) {
|
|
752
|
+
const char = content.charCodeAt(i)
|
|
753
|
+
hash = ((hash << 5) - hash) + char
|
|
754
|
+
hash = hash & hash
|
|
480
755
|
}
|
|
756
|
+
return hash.toString(36)
|
|
481
757
|
}
|
|
482
758
|
|
|
483
759
|
// Initial backup
|
|
484
760
|
backup()
|
|
485
761
|
|
|
486
762
|
// Set interval
|
|
487
|
-
const interval = setInterval(backup,
|
|
763
|
+
const interval = setInterval(backup, opts.interval * 60 * 1000)
|
|
488
764
|
|
|
489
765
|
// Handle Ctrl+C
|
|
490
766
|
process.on("SIGINT", () => {
|
|
491
767
|
clearInterval(interval)
|
|
492
768
|
console.log(`\n✅ Watch stopped. ${backupCount} backups created.\n`)
|
|
769
|
+
if (opts.logFile) writeWatchLog(opts.logPath, `Watch stopped. ${backupCount} backups created.`)
|
|
493
770
|
process.exit(0)
|
|
494
771
|
})
|
|
495
772
|
|
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
|
{
|
|
@@ -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
|
{
|
|
@@ -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
|
{
|
|
@@ -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
|
{
|
|
@@ -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
|
{
|