toon-memory 1.7.0 โ 2.0.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 +270 -34
- package/dist/cli/setup.js +409 -96
- package/mcp/server.js +268 -19
- package/package.json +9 -1
- package/skills/toon-memory.md +25 -14
- package/src/cli/setup.ts +650 -335
- package/src/mcp/server.ts +327 -11
- package/uninstall.sh +33 -5
package/src/cli/setup.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync, readdirSync, statSync } from "fs"
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync, readdirSync, statSync, chmodSync, rmSync } from "fs"
|
|
2
2
|
import { basename, dirname, join } from "path"
|
|
3
3
|
import { fileURLToPath } from "url"
|
|
4
4
|
import { createInterface } from "readline"
|
|
@@ -6,181 +6,277 @@ import { gzipSync, gunzipSync } from "zlib"
|
|
|
6
6
|
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
8
|
const projectRoot = process.cwd()
|
|
9
|
-
// When compiled to dist/cli/, src is at ../../src
|
|
10
|
-
// When running directly from src/cli/, src is at ../src
|
|
11
9
|
const sourceDir = join(__dirname, "..", "..", "src")
|
|
12
10
|
const HOME = process.env.HOME || process.env.USERPROFILE || "~"
|
|
13
11
|
|
|
12
|
+
/** Shared memory directory (agent-agnostic) */
|
|
13
|
+
const MEMORY_DIR = join(projectRoot, ".toon-memory", "memory")
|
|
14
|
+
|
|
15
|
+
/** Config format: "json" | "toml" | "jsonc" | "none" (instructions only) */
|
|
16
|
+
type AgentFormat = "json" | "toml" | "jsonc" | "none"
|
|
17
|
+
|
|
14
18
|
/** Supported AI coding agent configuration */
|
|
15
19
|
interface Agent {
|
|
16
|
-
/** Agent identifier (e.g., "opencode", "vscode/copilot") */
|
|
17
20
|
name: string
|
|
18
|
-
/** Global config file path (e.g., ~/.config/opencode/opencode.json) */
|
|
19
21
|
global?: string
|
|
20
|
-
/** Local (project-level) config file path (e.g., .opencode/opencode.json) */
|
|
21
22
|
local?: string
|
|
22
|
-
/** JSON key where MCP servers are stored */
|
|
23
23
|
mcpKey: string
|
|
24
|
+
format: AgentFormat
|
|
25
|
+
needsHooks: boolean
|
|
26
|
+
needsInstructions: boolean
|
|
27
|
+
instructionFile?: string
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/** Hook script content for SessionStart reminder */
|
|
31
|
+
const HOOK_CONTENT = `#!/bin/bash
|
|
32
|
+
echo "toon-memory: Use memory_recall BEFORE reading files for project context."
|
|
33
|
+
exit 0
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
/** Base instruction content for agents */
|
|
37
|
+
const INSTRUCTION_CONTENT = `# toon-memory
|
|
38
|
+
|
|
39
|
+
Persistent memory for this project. Use it to avoid re-investigating things.
|
|
40
|
+
|
|
41
|
+
## At the START of every session
|
|
42
|
+
1. Run memory_stats to see what's in memory.
|
|
43
|
+
2. If the user asks something that might be in memory, run memory_recall BEFORE reading files.
|
|
44
|
+
|
|
45
|
+
## When making decisions
|
|
46
|
+
- Before implementing a non-trivial change: memory_remember(category='decision')
|
|
47
|
+
- When you resolve a complex bug: memory_remember(category='bug')
|
|
48
|
+
- When you observe a code pattern: memory_remember(category='pattern')
|
|
49
|
+
|
|
50
|
+
## At the END of every session
|
|
51
|
+
- Save important decisions, bugs resolved, and patterns observed.
|
|
52
|
+
`
|
|
53
|
+
|
|
26
54
|
/**
|
|
27
55
|
* Detect all supported AI coding agents on the system.
|
|
28
|
-
*
|
|
56
|
+
*
|
|
29
57
|
* Scans for configuration files in both global (~/.config/) and local
|
|
30
58
|
* (.opencode/, .vscode/, etc.) locations.
|
|
31
|
-
*
|
|
59
|
+
*
|
|
32
60
|
* @returns Array of detected agent configurations
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* ```typescript
|
|
36
|
-
* const agents = detectAgents()
|
|
37
|
-
* for (const agent of agents) {
|
|
38
|
-
* console.log(`${agent.name}: ${agent.local || "not found"}`)
|
|
39
|
-
* }
|
|
40
|
-
* ```
|
|
41
61
|
*/
|
|
42
62
|
function detectAgents(): Agent[] {
|
|
43
63
|
const agents: Agent[] = []
|
|
44
|
-
|
|
64
|
+
|
|
45
65
|
// OpenCode
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
agents.push({
|
|
67
|
+
name: "opencode",
|
|
68
|
+
global: join(HOME, ".config", "opencode", "opencode.json"),
|
|
69
|
+
local: join(projectRoot, ".opencode", "opencode.json"),
|
|
70
|
+
mcpKey: "mcp",
|
|
71
|
+
format: "json",
|
|
72
|
+
needsHooks: false,
|
|
73
|
+
needsInstructions: true,
|
|
74
|
+
instructionFile: join(projectRoot, "AGENTS.md")
|
|
53
75
|
})
|
|
54
|
-
|
|
76
|
+
|
|
55
77
|
// VS Code / GitHub Copilot
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
agents.push({
|
|
79
|
+
name: "vscode/copilot",
|
|
80
|
+
local: join(projectRoot, ".vscode", "mcp.json"),
|
|
81
|
+
mcpKey: "servers",
|
|
82
|
+
format: "json",
|
|
83
|
+
needsHooks: false,
|
|
84
|
+
needsInstructions: false
|
|
61
85
|
})
|
|
62
|
-
|
|
86
|
+
|
|
63
87
|
// Claude Code
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
agents.push({
|
|
89
|
+
name: "claude",
|
|
90
|
+
global: join(HOME, ".claude", "settings.json"),
|
|
91
|
+
local: join(projectRoot, ".claude", "settings.json"),
|
|
92
|
+
mcpKey: "mcpServers",
|
|
93
|
+
format: "json",
|
|
94
|
+
needsHooks: true,
|
|
95
|
+
needsInstructions: true,
|
|
96
|
+
instructionFile: join(projectRoot, ".claude", "AGENTS.md")
|
|
71
97
|
})
|
|
72
|
-
|
|
98
|
+
|
|
73
99
|
// Cursor
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
agents.push({
|
|
101
|
+
name: "cursor",
|
|
102
|
+
local: join(projectRoot, ".cursor", "mcp.json"),
|
|
103
|
+
mcpKey: "mcpServers",
|
|
104
|
+
format: "json",
|
|
105
|
+
needsHooks: false,
|
|
106
|
+
needsInstructions: false
|
|
79
107
|
})
|
|
80
|
-
|
|
108
|
+
|
|
81
109
|
// Windsurf
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
agents.push({
|
|
111
|
+
name: "windsurf",
|
|
112
|
+
global: join(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
113
|
+
mcpKey: "mcpServers",
|
|
114
|
+
format: "json",
|
|
115
|
+
needsHooks: false,
|
|
116
|
+
needsInstructions: false
|
|
87
117
|
})
|
|
88
|
-
|
|
118
|
+
|
|
89
119
|
// Cline
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
120
|
+
agents.push({
|
|
121
|
+
name: "cline",
|
|
122
|
+
local: join(projectRoot, ".cline", "mcp.json"),
|
|
123
|
+
mcpKey: "mcpServers",
|
|
124
|
+
format: "json",
|
|
125
|
+
needsHooks: false,
|
|
126
|
+
needsInstructions: false
|
|
95
127
|
})
|
|
96
|
-
|
|
128
|
+
|
|
97
129
|
// Continue
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
agents.push({
|
|
131
|
+
name: "continue",
|
|
132
|
+
local: join(projectRoot, ".continue", "config.json"),
|
|
133
|
+
mcpKey: "mcpServers",
|
|
134
|
+
format: "json",
|
|
135
|
+
needsHooks: false,
|
|
136
|
+
needsInstructions: false
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Codex CLI
|
|
140
|
+
agents.push({
|
|
141
|
+
name: "codex",
|
|
142
|
+
local: join(projectRoot, ".codex", "config.toml"),
|
|
143
|
+
mcpKey: "mcpServers",
|
|
144
|
+
format: "toml",
|
|
145
|
+
needsHooks: true,
|
|
146
|
+
needsInstructions: true,
|
|
147
|
+
instructionFile: join(projectRoot, ".codex", "AGENTS.md")
|
|
103
148
|
})
|
|
104
|
-
|
|
149
|
+
|
|
150
|
+
// Gemini CLI
|
|
151
|
+
agents.push({
|
|
152
|
+
name: "gemini",
|
|
153
|
+
local: join(projectRoot, ".gemini", "settings.json"),
|
|
154
|
+
mcpKey: "mcpServers",
|
|
155
|
+
format: "json",
|
|
156
|
+
needsHooks: true,
|
|
157
|
+
needsInstructions: true,
|
|
158
|
+
instructionFile: join(projectRoot, ".gemini", "GEMINI.md")
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// Zed
|
|
162
|
+
agents.push({
|
|
163
|
+
name: "zed",
|
|
164
|
+
global: join(HOME, ".config", "zed", "settings.json"),
|
|
165
|
+
mcpKey: "mcp_servers",
|
|
166
|
+
format: "jsonc",
|
|
167
|
+
needsHooks: false,
|
|
168
|
+
needsInstructions: false
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Antigravity
|
|
172
|
+
agents.push({
|
|
173
|
+
name: "antigravity",
|
|
174
|
+
local: join(projectRoot, ".gemini", "config", "mcp_config.json"),
|
|
175
|
+
mcpKey: "mcpServers",
|
|
176
|
+
format: "json",
|
|
177
|
+
needsHooks: true,
|
|
178
|
+
needsInstructions: true,
|
|
179
|
+
instructionFile: join(projectRoot, "antigravity-cli", "AGENTS.md")
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Aider (instructions only, no MCP)
|
|
183
|
+
agents.push({
|
|
184
|
+
name: "aider",
|
|
185
|
+
mcpKey: "",
|
|
186
|
+
format: "none",
|
|
187
|
+
needsHooks: false,
|
|
188
|
+
needsInstructions: true,
|
|
189
|
+
instructionFile: join(projectRoot, "CONVENTIONS.md")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// KiloCode
|
|
193
|
+
agents.push({
|
|
194
|
+
name: "kilocode",
|
|
195
|
+
global: join(HOME, ".kilocode", "mcp_settings.json"),
|
|
196
|
+
mcpKey: "mcpServers",
|
|
197
|
+
format: "json",
|
|
198
|
+
needsHooks: false,
|
|
199
|
+
needsInstructions: true,
|
|
200
|
+
instructionFile: join(HOME, ".kilocode", "rules", "toon-memory.md")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// OpenClaw
|
|
204
|
+
agents.push({
|
|
205
|
+
name: "openclaw",
|
|
206
|
+
local: join(projectRoot, ".openclaw.json"),
|
|
207
|
+
mcpKey: "mcpServers",
|
|
208
|
+
format: "json",
|
|
209
|
+
needsHooks: false,
|
|
210
|
+
needsInstructions: false
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Kiro
|
|
214
|
+
agents.push({
|
|
215
|
+
name: "kiro",
|
|
216
|
+
local: join(projectRoot, ".kiro", "settings", "mcp.json"),
|
|
217
|
+
mcpKey: "mcpServers",
|
|
218
|
+
format: "json",
|
|
219
|
+
needsHooks: false,
|
|
220
|
+
needsInstructions: false
|
|
221
|
+
})
|
|
222
|
+
|
|
105
223
|
return agents
|
|
106
224
|
}
|
|
107
225
|
|
|
108
226
|
/**
|
|
109
|
-
* Install memory directory
|
|
110
|
-
*
|
|
111
|
-
* Creates `.
|
|
112
|
-
* The MCP server handles all memory operations
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```bash
|
|
116
|
-
* npx toon-memory init # Calls installOpenCodeTools()
|
|
117
|
-
* ```
|
|
227
|
+
* Install memory directory and initial data file.
|
|
228
|
+
*
|
|
229
|
+
* Creates `.toon-memory/memory/` directory and initial `data.toon` if needed.
|
|
230
|
+
* The MCP server handles all memory operations.
|
|
118
231
|
*/
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
const memoryFile = join(memoryDir, "data.toon")
|
|
232
|
+
function installMemoryDir(): void {
|
|
233
|
+
const memoryFile = join(MEMORY_DIR, "data.toon")
|
|
122
234
|
|
|
123
|
-
if (!existsSync(
|
|
235
|
+
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
|
124
236
|
|
|
125
237
|
if (!existsSync(memoryFile)) {
|
|
126
238
|
writeFileSync(memoryFile, "version: 1\n[0|]\n")
|
|
127
|
-
console.log(" Created .
|
|
239
|
+
console.log(" Created .toon-memory/memory/data.toon")
|
|
128
240
|
}
|
|
129
241
|
}
|
|
130
242
|
|
|
131
243
|
/**
|
|
132
|
-
* Add `.
|
|
133
|
-
*
|
|
134
|
-
* Creates `.gitignore` if it doesn't exist, or appends the memory
|
|
135
|
-
* exclusion pattern to the existing file.
|
|
136
|
-
*
|
|
137
|
-
* @example
|
|
138
|
-
* ```typescript
|
|
139
|
-
* ensureGitignore() // Adds .opencode/memory/ to .gitignore
|
|
140
|
-
* ```
|
|
244
|
+
* Add `.toon-memory/memory/` to `.gitignore` if not already present.
|
|
141
245
|
*/
|
|
142
246
|
function ensureGitignore(): void {
|
|
143
247
|
const gitignorePath = join(projectRoot, ".gitignore")
|
|
144
|
-
const entry = ".
|
|
145
|
-
|
|
248
|
+
const entry = ".toon-memory/memory/"
|
|
249
|
+
|
|
146
250
|
if (!existsSync(gitignorePath)) {
|
|
147
251
|
writeFileSync(gitignorePath, `${entry}\n`)
|
|
148
252
|
console.log(" Created .gitignore with memory exclusion")
|
|
149
253
|
return
|
|
150
254
|
}
|
|
151
|
-
|
|
255
|
+
|
|
152
256
|
const content = readFileSync(gitignorePath, "utf-8")
|
|
153
257
|
if (!content.includes(entry)) {
|
|
154
258
|
writeFileSync(gitignorePath, `${content.trim()}\n${entry}\n`)
|
|
155
|
-
console.log(" Added .
|
|
259
|
+
console.log(" Added .toon-memory/memory/ to .gitignore")
|
|
156
260
|
}
|
|
157
261
|
}
|
|
158
262
|
|
|
159
263
|
/**
|
|
160
|
-
* Install MCP server configuration for
|
|
161
|
-
*
|
|
264
|
+
* Install MCP server configuration for a JSON-format agent.
|
|
265
|
+
*
|
|
162
266
|
* Adds the `toon-memory` MCP server entry to the agent's config file.
|
|
163
|
-
*
|
|
164
|
-
* @param agent - Agent configuration with config path and MCP key
|
|
165
|
-
* @param scope - "global" or "local" installation scope
|
|
166
|
-
*
|
|
167
|
-
* @example
|
|
168
|
-
* ```typescript
|
|
169
|
-
* const agent = { name: "opencode", local: ".opencode/opencode.json", mcpKey: "mcp" }
|
|
170
|
-
* installMCPConfig(agent, "local")
|
|
171
|
-
* ```
|
|
267
|
+
* OpenCode uses a different format than other agents.
|
|
172
268
|
*/
|
|
173
|
-
function
|
|
269
|
+
function installJSONConfig(agent: Agent, scope: string): void {
|
|
174
270
|
const configPath = scope === "global" ? agent.global : agent.local
|
|
175
|
-
|
|
271
|
+
|
|
176
272
|
if (!configPath) {
|
|
177
273
|
console.log(` No ${scope} config path for ${agent.name}`)
|
|
178
274
|
return
|
|
179
275
|
}
|
|
180
|
-
|
|
276
|
+
|
|
181
277
|
const configDir = dirname(configPath)
|
|
182
278
|
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true })
|
|
183
|
-
|
|
279
|
+
|
|
184
280
|
let config: Record<string, any> = {}
|
|
185
281
|
if (existsSync(configPath)) {
|
|
186
282
|
try {
|
|
@@ -189,11 +285,10 @@ function installMCPConfig(agent: Agent, scope: string): void {
|
|
|
189
285
|
config = {}
|
|
190
286
|
}
|
|
191
287
|
}
|
|
192
|
-
|
|
288
|
+
|
|
193
289
|
const mcpKey = agent.mcpKey || "mcpServers"
|
|
194
290
|
if (!config[mcpKey]) config[mcpKey] = {}
|
|
195
|
-
|
|
196
|
-
// OpenCode uses a different format than other agents
|
|
291
|
+
|
|
197
292
|
if (agent.name === "opencode") {
|
|
198
293
|
config[mcpKey]["toon-memory"] = {
|
|
199
294
|
enabled: true,
|
|
@@ -206,103 +301,322 @@ function installMCPConfig(agent: Agent, scope: string): void {
|
|
|
206
301
|
args: ["-y", "toon-memory", "mcp"]
|
|
207
302
|
}
|
|
208
303
|
}
|
|
209
|
-
|
|
304
|
+
|
|
210
305
|
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
211
306
|
console.log(` MCP server added to ${configPath}`)
|
|
212
307
|
}
|
|
213
308
|
|
|
214
309
|
/**
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* @example
|
|
220
|
-
* ```bash
|
|
221
|
-
* npx toon-memory uninstall
|
|
222
|
-
* ```
|
|
310
|
+
* Install MCP server configuration for Codex CLI (TOML format).
|
|
311
|
+
*
|
|
312
|
+
* Writes a clean config.toml with the MCP server entry.
|
|
313
|
+
* If the file exists, it is overwritten.
|
|
223
314
|
*/
|
|
224
|
-
function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
315
|
+
function installTOMLConfig(agent: Agent): void {
|
|
316
|
+
const configPath = agent.local
|
|
317
|
+
if (!configPath) return
|
|
318
|
+
|
|
319
|
+
const configDir = dirname(configPath)
|
|
320
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true })
|
|
321
|
+
|
|
322
|
+
const toml = `[mcpServers.toon-memory]
|
|
323
|
+
command = "npx"
|
|
324
|
+
args = ["-y", "toon-memory", "mcp"]
|
|
325
|
+
`
|
|
326
|
+
|
|
327
|
+
writeFileSync(configPath, toml)
|
|
328
|
+
console.log(` MCP server added to ${configPath}`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Install MCP server configuration for Zed (JSONC format).
|
|
333
|
+
*
|
|
334
|
+
* Writes valid JSON to Zed's settings.json. If the file exists
|
|
335
|
+
* with comments, the user must merge manually.
|
|
336
|
+
*/
|
|
337
|
+
function installZedConfig(agent: Agent): void {
|
|
338
|
+
const configPath = agent.global
|
|
339
|
+
if (!configPath) return
|
|
340
|
+
|
|
341
|
+
const configDir = dirname(configPath)
|
|
342
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true })
|
|
343
|
+
|
|
344
|
+
let config: Record<string, any> = {}
|
|
345
|
+
if (existsSync(configPath)) {
|
|
346
|
+
try {
|
|
347
|
+
// Strip JSONC comments before parsing
|
|
348
|
+
const raw = readFileSync(configPath, "utf-8")
|
|
349
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "")
|
|
350
|
+
config = JSON.parse(stripped)
|
|
351
|
+
} catch {
|
|
352
|
+
config = {}
|
|
245
353
|
}
|
|
246
354
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
355
|
+
|
|
356
|
+
if (!config.mcp_servers) config.mcp_servers = {}
|
|
357
|
+
|
|
358
|
+
config.mcp_servers["toon-memory"] = {
|
|
359
|
+
command: "npx",
|
|
360
|
+
args: ["-y", "toon-memory", "mcp"]
|
|
253
361
|
}
|
|
254
|
-
|
|
255
|
-
|
|
362
|
+
|
|
363
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
364
|
+
console.log(` MCP server added to ${configPath}`)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Install MCP config for an agent based on its format.
|
|
369
|
+
*/
|
|
370
|
+
function installMCPConfig(agent: Agent, scope: string): void {
|
|
371
|
+
if (agent.format === "none") {
|
|
372
|
+
console.log(` ${agent.name}: instructions only (no MCP)`)
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (agent.format === "toml") {
|
|
377
|
+
installTOMLConfig(agent)
|
|
378
|
+
} else if (agent.format === "jsonc") {
|
|
379
|
+
installZedConfig(agent)
|
|
380
|
+
} else {
|
|
381
|
+
installJSONConfig(agent, scope)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Install instruction files for an agent.
|
|
387
|
+
*
|
|
388
|
+
* Creates AGENTS.md, GEMINI.md, CONVENTIONS.md, etc. with
|
|
389
|
+
* reminders to use toon-memory tools.
|
|
390
|
+
*/
|
|
391
|
+
function installInstructions(agent: Agent): void {
|
|
392
|
+
if (!agent.needsInstructions || !agent.instructionFile) return
|
|
393
|
+
|
|
394
|
+
const filePath = agent.instructionFile
|
|
395
|
+
const fileDir = dirname(filePath)
|
|
396
|
+
|
|
397
|
+
if (!existsSync(fileDir)) mkdirSync(fileDir, { recursive: true })
|
|
398
|
+
|
|
399
|
+
if (existsSync(filePath)) {
|
|
400
|
+
const existing = readFileSync(filePath, "utf-8")
|
|
401
|
+
if (existing.includes("toon-memory") || existing.includes("memory_recall")) {
|
|
402
|
+
console.log(` Instructions already present in ${filePath}`)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
// Append to existing file
|
|
406
|
+
writeFileSync(filePath, `${existing.trim()}\n\n${INSTRUCTION_CONTENT}`)
|
|
407
|
+
console.log(` Appended toon-memory instructions to ${filePath}`)
|
|
408
|
+
} else {
|
|
409
|
+
writeFileSync(filePath, INSTRUCTION_CONTENT)
|
|
410
|
+
console.log(` Created ${filePath}`)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Install SessionStart hook for agents that support it.
|
|
416
|
+
*
|
|
417
|
+
* Creates a shell script that reminds the agent to use memory tools,
|
|
418
|
+
* then registers it in the agent's config.
|
|
419
|
+
*/
|
|
420
|
+
function installHooks(agent: Agent): void {
|
|
421
|
+
if (!agent.needsHooks) return
|
|
422
|
+
|
|
423
|
+
const hookDir = join(projectRoot, ".toon-memory", "hooks")
|
|
424
|
+
if (!existsSync(hookDir)) mkdirSync(hookDir, { recursive: true })
|
|
425
|
+
|
|
426
|
+
const hookPath = join(hookDir, `session-start-${agent.name}.sh`)
|
|
427
|
+
writeFileSync(hookPath, HOOK_CONTENT)
|
|
428
|
+
chmodSync(hookPath, 0o755)
|
|
429
|
+
console.log(` Hook script created at ${hookPath}`)
|
|
430
|
+
|
|
431
|
+
// Register hook in agent config
|
|
432
|
+
if (agent.format === "toml" && agent.local) {
|
|
433
|
+
registerHookTOML(agent, hookPath)
|
|
434
|
+
} else if (agent.format === "json" || agent.format === "jsonc") {
|
|
435
|
+
registerHookJSON(agent, hookPath)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Register SessionStart hook in TOML config (Codex CLI) */
|
|
440
|
+
function registerHookTOML(agent: Agent, hookPath: string): void {
|
|
441
|
+
const configPath = agent.local
|
|
442
|
+
if (!configPath || !existsSync(configPath)) return
|
|
443
|
+
|
|
444
|
+
let content = readFileSync(configPath, "utf-8")
|
|
445
|
+
if (content.includes("session_start")) {
|
|
446
|
+
console.log(` Hook already registered in ${configPath}`)
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
content += `\n[hooks.session_start]\ncommand = "${hookPath}"\n`
|
|
451
|
+
writeFileSync(configPath, content)
|
|
452
|
+
console.log(` Hook registered in ${configPath}`)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Register SessionStart hook in JSON config */
|
|
456
|
+
function registerHookJSON(agent: Agent, hookPath: string): void {
|
|
457
|
+
const configPath = agent.format === "jsonc" ? agent.global : (agent.local || agent.global)
|
|
458
|
+
if (!configPath) return
|
|
459
|
+
|
|
460
|
+
const configDir = dirname(configPath)
|
|
461
|
+
if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true })
|
|
462
|
+
|
|
463
|
+
let config: Record<string, any> = {}
|
|
464
|
+
if (existsSync(configPath)) {
|
|
465
|
+
try {
|
|
466
|
+
if (agent.format === "jsonc") {
|
|
467
|
+
const raw = readFileSync(configPath, "utf-8")
|
|
468
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "")
|
|
469
|
+
config = JSON.parse(stripped)
|
|
470
|
+
} else {
|
|
471
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
config = {}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Claude Code format
|
|
479
|
+
if (agent.name === "claude") {
|
|
480
|
+
if (!config.hooks) config.hooks = {}
|
|
481
|
+
if (!config.hooks.SessionStart) config.hooks.SessionStart = []
|
|
482
|
+
if (!config.hooks.SessionStart.some((h: any) => h.command === hookPath)) {
|
|
483
|
+
config.hooks.SessionStart.push({ command: hookPath })
|
|
484
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
485
|
+
console.log(` Hook registered in ${configPath}`)
|
|
486
|
+
}
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Gemini CLI / Antigravity format
|
|
491
|
+
if (!config.session_start_hooks) config.session_start_hooks = []
|
|
492
|
+
if (!config.session_start_hooks.includes(hookPath)) {
|
|
493
|
+
config.session_start_hooks.push(hookPath)
|
|
494
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
495
|
+
console.log(` Hook registered in ${configPath}`)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Install everything for a single agent.
|
|
501
|
+
*/
|
|
502
|
+
function installForAgent(agent: Agent, scope: string): void {
|
|
503
|
+
console.log(`${agent.name}:`)
|
|
504
|
+
installMCPConfig(agent, scope)
|
|
505
|
+
installInstructions(agent)
|
|
506
|
+
installHooks(agent)
|
|
256
507
|
}
|
|
257
508
|
|
|
258
509
|
/**
|
|
259
510
|
* Initialize toon-memory for all detected agents (non-interactive).
|
|
260
|
-
*
|
|
511
|
+
*
|
|
261
512
|
* Installs MCP server configs, creates memory directory, and
|
|
262
513
|
* updates `.gitignore`.
|
|
263
|
-
*
|
|
264
|
-
* @param scope - "local" (default) or "global" installation scope
|
|
265
|
-
*
|
|
266
|
-
* @example
|
|
267
|
-
* ```bash
|
|
268
|
-
* npx toon-memory init # Local install
|
|
269
|
-
* npx toon-memory init global # Global install
|
|
270
|
-
* ```
|
|
271
514
|
*/
|
|
272
515
|
function init(scope: string = "local"): void {
|
|
273
516
|
console.log("\n๐ง toon-memory init\n")
|
|
274
|
-
|
|
517
|
+
|
|
518
|
+
installMemoryDir()
|
|
519
|
+
|
|
275
520
|
const agents = detectAgents()
|
|
276
|
-
|
|
277
521
|
for (const agent of agents) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (agent.name === "opencode") {
|
|
281
|
-
installOpenCodeTools()
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
installMCPConfig(agent, scope)
|
|
522
|
+
installForAgent(agent, scope)
|
|
285
523
|
console.log("")
|
|
286
524
|
}
|
|
287
|
-
|
|
525
|
+
|
|
288
526
|
ensureGitignore()
|
|
289
|
-
|
|
527
|
+
|
|
290
528
|
console.log("Done! Restart your agent to use memory tools.\n")
|
|
291
529
|
}
|
|
292
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Uninstall toon-memory from all detected agents.
|
|
533
|
+
*
|
|
534
|
+
* Removes MCP server configurations, instruction files, and hooks.
|
|
535
|
+
*/
|
|
536
|
+
function uninstall(): void {
|
|
537
|
+
console.log("\n๐ง toon-memory uninstaller\n")
|
|
538
|
+
|
|
539
|
+
const agents = detectAgents()
|
|
540
|
+
|
|
541
|
+
for (const agent of agents) {
|
|
542
|
+
// Remove MCP config from JSON files
|
|
543
|
+
if (agent.format === "json" || agent.format === "jsonc") {
|
|
544
|
+
const configs = [agent.global, agent.local].filter(Boolean) as string[]
|
|
545
|
+
|
|
546
|
+
for (const configPath of configs) {
|
|
547
|
+
if (!existsSync(configPath)) continue
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
let config: Record<string, any>
|
|
551
|
+
if (agent.format === "jsonc") {
|
|
552
|
+
const raw = readFileSync(configPath, "utf-8")
|
|
553
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "")
|
|
554
|
+
config = JSON.parse(stripped)
|
|
555
|
+
} else {
|
|
556
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const mcpKey = agent.mcpKey || "mcpServers"
|
|
560
|
+
|
|
561
|
+
if (config[mcpKey]?.["toon-memory"]) {
|
|
562
|
+
delete config[mcpKey]["toon-memory"]
|
|
563
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
564
|
+
console.log(` Removed MCP from ${agent.name} (${configPath})`)
|
|
565
|
+
}
|
|
566
|
+
} catch {}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Remove TOML config
|
|
571
|
+
if (agent.format === "toml" && agent.local && existsSync(agent.local)) {
|
|
572
|
+
try {
|
|
573
|
+
let content = readFileSync(agent.local, "utf-8")
|
|
574
|
+
content = content.replace(/\[mcpServers\.toon-memory\][\s\S]*?(?=\n\[|$)/, "").trim() + "\n"
|
|
575
|
+
writeFileSync(agent.local, content)
|
|
576
|
+
console.log(` Removed MCP from ${agent.name} (${agent.local})`)
|
|
577
|
+
} catch {}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Remove instruction files
|
|
582
|
+
for (const agent of agents) {
|
|
583
|
+
if (agent.instructionFile && existsSync(agent.instructionFile)) {
|
|
584
|
+
try {
|
|
585
|
+
const content = readFileSync(agent.instructionFile, "utf-8")
|
|
586
|
+
// Only remove if it's purely toon-memory instructions
|
|
587
|
+
if (content.includes(INSTRUCTION_CONTENT.trim()) && content.length < INSTRUCTION_CONTENT.length + 100) {
|
|
588
|
+
unlinkSync(agent.instructionFile)
|
|
589
|
+
console.log(` Removed ${agent.instructionFile}`)
|
|
590
|
+
}
|
|
591
|
+
} catch {}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Remove hook scripts
|
|
596
|
+
const hookDir = join(projectRoot, ".toon-memory", "hooks")
|
|
597
|
+
if (existsSync(hookDir)) {
|
|
598
|
+
rmSync(hookDir, { recursive: true, force: true })
|
|
599
|
+
console.log(" Removed .toon-memory/hooks/")
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Remove legacy .opencode/tools/ if exists
|
|
603
|
+
const toolsFile = join(projectRoot, ".opencode", "tools", "memory.ts")
|
|
604
|
+
if (existsSync(toolsFile)) {
|
|
605
|
+
unlinkSync(toolsFile)
|
|
606
|
+
console.log(" Removed .opencode/tools/memory.ts")
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
console.log("\nโ
toon-memory uninstalled from all agents\n")
|
|
610
|
+
}
|
|
611
|
+
|
|
293
612
|
/**
|
|
294
613
|
* Show toon-memory installation status.
|
|
295
|
-
*
|
|
614
|
+
*
|
|
296
615
|
* Displays version, memory entry count, and agent configuration status.
|
|
297
|
-
*
|
|
298
|
-
* @example
|
|
299
|
-
* ```bash
|
|
300
|
-
* npx toon-memory status
|
|
301
|
-
* ```
|
|
302
616
|
*/
|
|
303
617
|
function status(): void {
|
|
304
618
|
console.log("\n๐ง toon-memory status\n")
|
|
305
|
-
|
|
619
|
+
|
|
306
620
|
// Check npm package
|
|
307
621
|
try {
|
|
308
622
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"))
|
|
@@ -310,9 +624,9 @@ function status(): void {
|
|
|
310
624
|
} catch {
|
|
311
625
|
console.log("Version: unknown")
|
|
312
626
|
}
|
|
313
|
-
|
|
627
|
+
|
|
314
628
|
// Check memory file
|
|
315
|
-
const memoryFile = join(
|
|
629
|
+
const memoryFile = join(MEMORY_DIR, "data.toon")
|
|
316
630
|
if (existsSync(memoryFile)) {
|
|
317
631
|
const data = readFileSync(memoryFile, "utf-8")
|
|
318
632
|
const lines = data.split("\n").filter((l: string) => l.startsWith(" ") && l.includes("|"))
|
|
@@ -320,54 +634,74 @@ function status(): void {
|
|
|
320
634
|
} else {
|
|
321
635
|
console.log("Memory: not initialized")
|
|
322
636
|
}
|
|
323
|
-
|
|
637
|
+
|
|
324
638
|
// Check agent configs
|
|
325
639
|
const agents = detectAgents()
|
|
326
640
|
console.log("\nAgent configs:")
|
|
327
|
-
|
|
641
|
+
|
|
328
642
|
for (const agent of agents) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
643
|
+
let configured = false
|
|
644
|
+
|
|
645
|
+
// Check MCP config
|
|
646
|
+
if (agent.format === "json" || agent.format === "jsonc") {
|
|
647
|
+
const configs = [agent.global, agent.local].filter(Boolean) as string[]
|
|
648
|
+
|
|
649
|
+
for (const configPath of configs) {
|
|
650
|
+
if (!existsSync(configPath)) continue
|
|
651
|
+
|
|
652
|
+
try {
|
|
653
|
+
let config: Record<string, any>
|
|
654
|
+
if (agent.format === "jsonc") {
|
|
655
|
+
const raw = readFileSync(configPath, "utf-8")
|
|
656
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "")
|
|
657
|
+
config = JSON.parse(stripped)
|
|
658
|
+
} else {
|
|
659
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"))
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (config[agent.mcpKey]?.["toon-memory"]) {
|
|
663
|
+
configured = true
|
|
664
|
+
}
|
|
665
|
+
} catch {}
|
|
666
|
+
}
|
|
344
667
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
668
|
+
|
|
669
|
+
// Check TOML
|
|
670
|
+
if (agent.format === "toml" && agent.local && existsSync(agent.local)) {
|
|
671
|
+
const content = readFileSync(agent.local, "utf-8")
|
|
672
|
+
if (content.includes("toon-memory")) configured = true
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Check instructions
|
|
676
|
+
const hasInstructions = agent.instructionFile ? existsSync(agent.instructionFile) : false
|
|
677
|
+
|
|
678
|
+
// Check hooks
|
|
679
|
+
const hookPath = join(projectRoot, ".toon-memory", "hooks", `session-start-${agent.name}.sh`)
|
|
680
|
+
const hasHooks = existsSync(hookPath)
|
|
681
|
+
|
|
682
|
+
if (agent.format === "none") {
|
|
683
|
+
console.log(` ${hasInstructions ? "โ
" : "โ"} ${agent.name} (instructions only)`)
|
|
684
|
+
} else {
|
|
685
|
+
const mcpStatus = configured ? "โ
" : "โ"
|
|
686
|
+
const instrStatus = agent.needsInstructions ? (hasInstructions ? " ๐" : "") : ""
|
|
687
|
+
const hookStatus = agent.needsHooks ? (hasHooks ? " ๐ช" : "") : ""
|
|
688
|
+
console.log(` ${mcpStatus} ${agent.name}${instrStatus}${hookStatus}`)
|
|
348
689
|
}
|
|
349
690
|
}
|
|
350
|
-
|
|
691
|
+
|
|
351
692
|
console.log("")
|
|
352
693
|
}
|
|
353
694
|
|
|
354
695
|
/**
|
|
355
696
|
* Upgrade toon-memory to the latest version.
|
|
356
|
-
*
|
|
357
|
-
* Checks npm registry for updates and installs if available.
|
|
358
|
-
*
|
|
359
|
-
* @example
|
|
360
|
-
* ```bash
|
|
361
|
-
* npx toon-memory upgrade
|
|
362
|
-
* ```
|
|
363
697
|
*/
|
|
364
698
|
function upgrade(): void {
|
|
365
699
|
console.log("\n๐ง toon-memory upgrade\n")
|
|
366
|
-
|
|
700
|
+
|
|
367
701
|
try {
|
|
368
702
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"))
|
|
369
703
|
const currentVersion = pkg.version
|
|
370
|
-
|
|
704
|
+
|
|
371
705
|
console.log(`Current version: ${currentVersion}`)
|
|
372
706
|
console.log("\nTo upgrade, run:")
|
|
373
707
|
console.log(" npm install -g toon-memory@latest")
|
|
@@ -381,30 +715,23 @@ function upgrade(): void {
|
|
|
381
715
|
|
|
382
716
|
/**
|
|
383
717
|
* Display memory statistics.
|
|
384
|
-
*
|
|
385
|
-
* Shows entry counts by category, last update date, and file size.
|
|
386
|
-
*
|
|
387
|
-
* @example
|
|
388
|
-
* ```bash
|
|
389
|
-
* npx toon-memory stats
|
|
390
|
-
* ```
|
|
391
718
|
*/
|
|
392
719
|
function stats(): void {
|
|
393
720
|
console.log("\n๐ง toon-memory stats\n")
|
|
394
|
-
|
|
395
|
-
const memoryFile = join(
|
|
396
|
-
|
|
721
|
+
|
|
722
|
+
const memoryFile = join(MEMORY_DIR, "data.toon")
|
|
723
|
+
|
|
397
724
|
if (!existsSync(memoryFile)) {
|
|
398
725
|
console.log("Memory not initialized. Run 'npx toon-memory init' first.\n")
|
|
399
726
|
return
|
|
400
727
|
}
|
|
401
|
-
|
|
728
|
+
|
|
402
729
|
const data = readFileSync(memoryFile, "utf-8")
|
|
403
730
|
const lines = data.split("\n").filter((l: string) => l.startsWith(" ") && l.includes("|"))
|
|
404
|
-
|
|
731
|
+
|
|
405
732
|
const categories: Record<string, number> = {}
|
|
406
733
|
let latestDate = ""
|
|
407
|
-
|
|
734
|
+
|
|
408
735
|
for (const line of lines) {
|
|
409
736
|
const parts = line.trim().split("|")
|
|
410
737
|
if (parts.length >= 7) {
|
|
@@ -414,17 +741,17 @@ function stats(): void {
|
|
|
414
741
|
if (date > latestDate) latestDate = date
|
|
415
742
|
}
|
|
416
743
|
}
|
|
417
|
-
|
|
744
|
+
|
|
418
745
|
console.log("๐ Memory Stats")
|
|
419
746
|
console.log("โ".repeat(20))
|
|
420
747
|
console.log(`Total entries: ${lines.length}`)
|
|
421
|
-
|
|
748
|
+
|
|
422
749
|
for (const [cat, count] of Object.entries(categories)) {
|
|
423
750
|
console.log(`โโโ ${cat}: ${count}`)
|
|
424
751
|
}
|
|
425
|
-
|
|
752
|
+
|
|
426
753
|
console.log(`Last updated: ${latestDate || "never"}`)
|
|
427
|
-
|
|
754
|
+
|
|
428
755
|
const fileSize = Buffer.byteLength(data, "utf-8")
|
|
429
756
|
console.log(`File size: ${(fileSize / 1024).toFixed(1)} KB`)
|
|
430
757
|
console.log("")
|
|
@@ -432,28 +759,20 @@ function stats(): void {
|
|
|
432
759
|
|
|
433
760
|
/**
|
|
434
761
|
* Export memory to JSON format.
|
|
435
|
-
*
|
|
436
|
-
* Creates a `toon-memory-export.json` file with all entries
|
|
437
|
-
* for backup or transfer to another project.
|
|
438
|
-
*
|
|
439
|
-
* @example
|
|
440
|
-
* ```bash
|
|
441
|
-
* npx toon-memory export
|
|
442
|
-
* ```
|
|
443
762
|
*/
|
|
444
763
|
function exportMemory(): void {
|
|
445
764
|
console.log("\n๐ง toon-memory export\n")
|
|
446
|
-
|
|
447
|
-
const memoryFile = join(
|
|
448
|
-
|
|
765
|
+
|
|
766
|
+
const memoryFile = join(MEMORY_DIR, "data.toon")
|
|
767
|
+
|
|
449
768
|
if (!existsSync(memoryFile)) {
|
|
450
769
|
console.log("Memory not initialized. Run 'npx toon-memory init' first.\n")
|
|
451
770
|
return
|
|
452
771
|
}
|
|
453
|
-
|
|
772
|
+
|
|
454
773
|
const data = readFileSync(memoryFile, "utf-8")
|
|
455
774
|
const lines = data.split("\n").filter((l: string) => l.startsWith(" ") && l.includes("|"))
|
|
456
|
-
|
|
775
|
+
|
|
457
776
|
const entries = lines.map((line: string) => {
|
|
458
777
|
const parts = line.trim().split("|")
|
|
459
778
|
return {
|
|
@@ -466,51 +785,41 @@ function exportMemory(): void {
|
|
|
466
785
|
date: parts[6]
|
|
467
786
|
}
|
|
468
787
|
})
|
|
469
|
-
|
|
788
|
+
|
|
470
789
|
const exportData = {
|
|
471
790
|
project: basename(projectRoot),
|
|
472
791
|
exported_at: new Date().toISOString(),
|
|
473
792
|
entries,
|
|
474
793
|
summaries: {}
|
|
475
794
|
}
|
|
476
|
-
|
|
795
|
+
|
|
477
796
|
const outputPath = join(projectRoot, "toon-memory-export.json")
|
|
478
797
|
writeFileSync(outputPath, JSON.stringify(exportData, null, 2))
|
|
479
|
-
|
|
798
|
+
|
|
480
799
|
console.log(`Exported ${entries.length} entries to:`)
|
|
481
800
|
console.log(` ${outputPath}\n`)
|
|
482
801
|
}
|
|
483
802
|
|
|
484
803
|
/**
|
|
485
804
|
* Import memory from JSON file.
|
|
486
|
-
*
|
|
487
|
-
* Imports entries from a JSON export, skipping duplicates based on key.
|
|
488
|
-
*
|
|
489
|
-
* @param file - Path to JSON file (relative or absolute)
|
|
490
|
-
*
|
|
491
|
-
* @example
|
|
492
|
-
* ```bash
|
|
493
|
-
* npx toon-memory import toon-memory-export.json
|
|
494
|
-
* ```
|
|
495
805
|
*/
|
|
496
806
|
function importMemory(): void {
|
|
497
807
|
console.log("\n๐ง toon-memory import\n")
|
|
498
|
-
|
|
808
|
+
|
|
499
809
|
const importFile = process.argv[3]
|
|
500
|
-
|
|
810
|
+
|
|
501
811
|
if (!importFile) {
|
|
502
812
|
console.log("Usage: npx toon-memory import <file.json>\n")
|
|
503
813
|
return
|
|
504
814
|
}
|
|
505
|
-
|
|
506
|
-
// Use absolute path if provided, otherwise resolve relative to project root
|
|
815
|
+
|
|
507
816
|
const importPath = importFile.startsWith("/") ? importFile : join(projectRoot, importFile)
|
|
508
|
-
|
|
817
|
+
|
|
509
818
|
if (!existsSync(importPath)) {
|
|
510
819
|
console.log(`File not found: ${importPath}\n`)
|
|
511
820
|
return
|
|
512
821
|
}
|
|
513
|
-
|
|
822
|
+
|
|
514
823
|
let importData: any
|
|
515
824
|
try {
|
|
516
825
|
importData = JSON.parse(readFileSync(importPath, "utf-8"))
|
|
@@ -518,17 +827,16 @@ function importMemory(): void {
|
|
|
518
827
|
console.log("Invalid JSON file\n")
|
|
519
828
|
return
|
|
520
829
|
}
|
|
521
|
-
|
|
830
|
+
|
|
522
831
|
if (!importData.entries || !Array.isArray(importData.entries)) {
|
|
523
832
|
console.log("Invalid format: missing 'entries' array\n")
|
|
524
833
|
return
|
|
525
834
|
}
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
835
|
+
|
|
836
|
+
const memoryFile = join(MEMORY_DIR, "data.toon")
|
|
837
|
+
|
|
838
|
+
if (!existsSync(MEMORY_DIR)) mkdirSync(MEMORY_DIR, { recursive: true })
|
|
839
|
+
|
|
532
840
|
let existingKeys: string[] = []
|
|
533
841
|
if (existsSync(memoryFile)) {
|
|
534
842
|
const existing = readFileSync(memoryFile, "utf-8")
|
|
@@ -536,45 +844,42 @@ function importMemory(): void {
|
|
|
536
844
|
.filter((l: string) => l.startsWith(" ") && l.includes("|"))
|
|
537
845
|
.map((l: string) => l.trim().split("|")[2])
|
|
538
846
|
}
|
|
539
|
-
|
|
847
|
+
|
|
540
848
|
const newEntries = importData.entries.filter((e: any) => !existingKeys.includes(e.key))
|
|
541
|
-
|
|
849
|
+
|
|
542
850
|
if (newEntries.length === 0) {
|
|
543
851
|
console.log("No new entries to import (all keys already exist)\n")
|
|
544
852
|
return
|
|
545
853
|
}
|
|
546
|
-
|
|
854
|
+
|
|
547
855
|
const newLines = newEntries.map((e: any) => {
|
|
548
856
|
const tags = Array.isArray(e.tags) ? e.tags.join(";") : (e.tags || "")
|
|
549
857
|
return ` ${e.id}|${e.category}|${e.key}|${e.content}|${e.file}|${tags}|${e.date}`
|
|
550
858
|
}).join("\n")
|
|
551
|
-
|
|
859
|
+
|
|
552
860
|
if (existsSync(memoryFile)) {
|
|
553
861
|
const existing = readFileSync(memoryFile, "utf-8")
|
|
862
|
+
const existingCount = existing.split("\n")
|
|
863
|
+
.filter((l: string) => l.startsWith(" ") && l.includes("|")).length
|
|
554
864
|
const updated = existing.replace(
|
|
555
865
|
/entries\[\d+\|]/,
|
|
556
|
-
`entries[${newEntries.length}|]`
|
|
866
|
+
`entries[${existingCount + newEntries.length}|]`
|
|
557
867
|
) + "\n" + newLines
|
|
558
868
|
writeFileSync(memoryFile, updated)
|
|
559
869
|
} else {
|
|
560
870
|
writeFileSync(memoryFile, `version: 1\nentries[${newEntries.length}|]{id|category|key|content|file|tags|date}:\n${newLines}\n`)
|
|
561
871
|
}
|
|
562
|
-
|
|
872
|
+
|
|
563
873
|
console.log(`Imported ${newEntries.length} new entries`)
|
|
564
874
|
console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates\n`)
|
|
565
875
|
}
|
|
566
876
|
|
|
567
877
|
/** Watch mode options */
|
|
568
878
|
interface WatchOptions {
|
|
569
|
-
/** Backup interval in minutes (default: 5) */
|
|
570
879
|
interval: number
|
|
571
|
-
/** Maximum number of backups to keep (0 = unlimited) */
|
|
572
880
|
maxBackups: number
|
|
573
|
-
/** Enable gzip compression for backups */
|
|
574
881
|
compress: boolean
|
|
575
|
-
/** Enable file logging */
|
|
576
882
|
logFile: boolean
|
|
577
|
-
/** Log file path */
|
|
578
883
|
logPath: string
|
|
579
884
|
}
|
|
580
885
|
|
|
@@ -587,21 +892,21 @@ function parseWatchOptions(args: string[]): WatchOptions {
|
|
|
587
892
|
logFile: false,
|
|
588
893
|
logPath: ""
|
|
589
894
|
}
|
|
590
|
-
|
|
895
|
+
|
|
591
896
|
for (let i = 1; i < args.length; i++) {
|
|
592
897
|
const arg = args[i]
|
|
593
898
|
if (arg === "--compress" || arg === "-c") {
|
|
594
899
|
opts.compress = true
|
|
595
900
|
} else if (arg === "--log" || arg === "-l") {
|
|
596
901
|
opts.logFile = true
|
|
597
|
-
opts.logPath = args[++i] || join(
|
|
902
|
+
opts.logPath = args[++i] || join(MEMORY_DIR, "watch.log")
|
|
598
903
|
} else if (arg === "--max-backups" || arg === "-m") {
|
|
599
904
|
opts.maxBackups = parseInt(args[++i]) || 10
|
|
600
905
|
} else if (!arg.startsWith("-")) {
|
|
601
906
|
opts.interval = parseInt(arg) || 5
|
|
602
907
|
}
|
|
603
908
|
}
|
|
604
|
-
|
|
909
|
+
|
|
605
910
|
return opts
|
|
606
911
|
}
|
|
607
912
|
|
|
@@ -616,7 +921,7 @@ function writeWatchLog(logPath: string, message: string): void {
|
|
|
616
921
|
/** Get list of backup files sorted by creation time (oldest first) */
|
|
617
922
|
function getBackupFiles(backupDir: string): string[] {
|
|
618
923
|
if (!existsSync(backupDir)) return []
|
|
619
|
-
|
|
924
|
+
|
|
620
925
|
return readdirSync(backupDir)
|
|
621
926
|
.filter(f => f.startsWith("backup-") && (f.endsWith(".toon") || f.endsWith(".gz")))
|
|
622
927
|
.map(f => join(backupDir, f))
|
|
@@ -626,16 +931,16 @@ function getBackupFiles(backupDir: string): string[] {
|
|
|
626
931
|
/** Remove oldest backups if we exceed maxBackups */
|
|
627
932
|
function pruneBackups(backupDir: string, maxBackups: number): number {
|
|
628
933
|
if (maxBackups <= 0) return 0
|
|
629
|
-
|
|
934
|
+
|
|
630
935
|
const files = getBackupFiles(backupDir)
|
|
631
936
|
const excess = files.length - maxBackups
|
|
632
|
-
|
|
937
|
+
|
|
633
938
|
if (excess <= 0) return 0
|
|
634
|
-
|
|
939
|
+
|
|
635
940
|
for (let i = 0; i < excess; i++) {
|
|
636
941
|
unlinkSync(files[i])
|
|
637
942
|
}
|
|
638
|
-
|
|
943
|
+
|
|
639
944
|
return excess
|
|
640
945
|
}
|
|
641
946
|
|
|
@@ -651,72 +956,62 @@ function decompressData(data: Buffer): string {
|
|
|
651
956
|
|
|
652
957
|
/**
|
|
653
958
|
* Watch mode - backup memory every N minutes
|
|
654
|
-
*
|
|
655
|
-
* @example
|
|
656
|
-
* ```bash
|
|
657
|
-
* npx toon-memory watch # Default: 5 min interval, 10 max backups
|
|
658
|
-
* npx toon-memory watch 10 # 10 minute interval
|
|
659
|
-
* npx toon-memory watch -c # Enable compression
|
|
660
|
-
* npx toon-memory watch -l # Enable file logging
|
|
661
|
-
* npx toon-memory watch -m 20 # Keep max 20 backups
|
|
662
|
-
* npx toon-memory watch 15 -c -l -m 5 # All options
|
|
663
|
-
* ```
|
|
664
959
|
*/
|
|
665
960
|
function watch(): void {
|
|
666
961
|
console.log("\n๐ง toon-memory watch\n")
|
|
667
|
-
|
|
668
|
-
const memoryFile = join(
|
|
669
|
-
const backupDir = join(
|
|
670
|
-
|
|
962
|
+
|
|
963
|
+
const memoryFile = join(MEMORY_DIR, "data.toon")
|
|
964
|
+
const backupDir = join(MEMORY_DIR, "backups")
|
|
965
|
+
|
|
671
966
|
if (!existsSync(memoryFile)) {
|
|
672
967
|
console.log("Memory not initialized. Run 'npx toon-memory init' first.\n")
|
|
673
968
|
return
|
|
674
969
|
}
|
|
675
|
-
|
|
970
|
+
|
|
676
971
|
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true })
|
|
677
|
-
|
|
972
|
+
|
|
678
973
|
const opts = parseWatchOptions(args)
|
|
679
|
-
|
|
974
|
+
|
|
680
975
|
if (opts.logFile) {
|
|
681
976
|
const logDir = dirname(opts.logPath)
|
|
682
977
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
|
|
683
978
|
}
|
|
684
|
-
|
|
979
|
+
|
|
685
980
|
console.log(`Watching memory file every ${opts.interval} minutes...`)
|
|
686
981
|
console.log(`Max backups: ${opts.maxBackups === 0 ? "unlimited" : opts.maxBackups}`)
|
|
687
982
|
console.log(`Compression: ${opts.compress ? "enabled" : "disabled"}`)
|
|
688
983
|
console.log(`Logging: ${opts.logFile ? `enabled (${opts.logPath})` : "disabled"}`)
|
|
689
984
|
console.log(`Press Ctrl+C to stop\n`)
|
|
690
|
-
|
|
985
|
+
|
|
691
986
|
let lastContent = readFileSync(memoryFile, "utf-8")
|
|
692
987
|
let lastHash = hashContent(lastContent)
|
|
693
988
|
let backupCount = 0
|
|
694
|
-
|
|
989
|
+
|
|
695
990
|
if (opts.logFile) writeWatchLog(opts.logPath, "Watch started")
|
|
696
|
-
|
|
991
|
+
|
|
697
992
|
const backup = () => {
|
|
698
993
|
try {
|
|
699
994
|
const currentContent = readFileSync(memoryFile, "utf-8")
|
|
700
995
|
const currentHash = hashContent(currentContent)
|
|
701
|
-
|
|
996
|
+
|
|
702
997
|
if (currentHash !== lastHash) {
|
|
703
998
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
704
999
|
const ext = opts.compress ? ".toon.gz" : ".toon"
|
|
705
1000
|
const backupFile = join(backupDir, `backup-${timestamp}${ext}`)
|
|
706
|
-
|
|
1001
|
+
|
|
707
1002
|
if (opts.compress) {
|
|
708
1003
|
writeFileSync(backupFile, compressData(currentContent))
|
|
709
1004
|
} else {
|
|
710
1005
|
writeFileSync(backupFile, currentContent)
|
|
711
1006
|
}
|
|
712
|
-
|
|
1007
|
+
|
|
713
1008
|
backupCount++
|
|
714
1009
|
console.log(`๐ฆ Backup #${backupCount} created: ${timestamp}`)
|
|
715
1010
|
if (opts.logFile) writeWatchLog(opts.logPath, `Backup #${backupCount}: ${timestamp}`)
|
|
716
|
-
|
|
1011
|
+
|
|
717
1012
|
lastContent = currentContent
|
|
718
1013
|
lastHash = currentHash
|
|
719
|
-
|
|
1014
|
+
|
|
720
1015
|
const pruned = pruneBackups(backupDir, opts.maxBackups)
|
|
721
1016
|
if (pruned > 0) {
|
|
722
1017
|
console.log(`๐๏ธ Pruned ${pruned} old backup(s)`)
|
|
@@ -729,7 +1024,7 @@ function watch(): void {
|
|
|
729
1024
|
if (opts.logFile) writeWatchLog(opts.logPath, msg)
|
|
730
1025
|
}
|
|
731
1026
|
}
|
|
732
|
-
|
|
1027
|
+
|
|
733
1028
|
/** Simple hash for change detection (not cryptographic) */
|
|
734
1029
|
function hashContent(content: string): string {
|
|
735
1030
|
let hash = 0
|
|
@@ -740,21 +1035,17 @@ function watch(): void {
|
|
|
740
1035
|
}
|
|
741
1036
|
return hash.toString(36)
|
|
742
1037
|
}
|
|
743
|
-
|
|
744
|
-
// Initial backup
|
|
1038
|
+
|
|
745
1039
|
backup()
|
|
746
|
-
|
|
747
|
-
// Set interval
|
|
748
1040
|
const interval = setInterval(backup, opts.interval * 60 * 1000)
|
|
749
|
-
|
|
750
|
-
// Handle Ctrl+C
|
|
1041
|
+
|
|
751
1042
|
process.on("SIGINT", () => {
|
|
752
1043
|
clearInterval(interval)
|
|
753
1044
|
console.log(`\nโ
Watch stopped. ${backupCount} backups created.\n`)
|
|
754
1045
|
if (opts.logFile) writeWatchLog(opts.logPath, `Watch stopped. ${backupCount} backups created.`)
|
|
755
1046
|
process.exit(0)
|
|
756
1047
|
})
|
|
757
|
-
|
|
1048
|
+
|
|
758
1049
|
process.on("SIGTERM", () => {
|
|
759
1050
|
clearInterval(interval)
|
|
760
1051
|
process.exit(0)
|
|
@@ -804,32 +1095,56 @@ if (args[0] === "watch") {
|
|
|
804
1095
|
process.exit(0)
|
|
805
1096
|
}
|
|
806
1097
|
|
|
1098
|
+
// Interactive installer
|
|
807
1099
|
const agents = detectAgents()
|
|
808
1100
|
console.log("\n๐ง toon-memory installer\n")
|
|
809
1101
|
|
|
810
|
-
console.log("
|
|
811
|
-
agents.forEach((a: Agent, i: number) =>
|
|
1102
|
+
console.log("Available agents:")
|
|
1103
|
+
agents.forEach((a: Agent, i: number) => {
|
|
1104
|
+
const hasConfig = a.format !== "none" && ((a.local && existsSync(a.local)) || (a.global && existsSync(a.global)))
|
|
1105
|
+
const indicator = hasConfig ? "โ" : "ยท"
|
|
1106
|
+
console.log(` ${indicator} ${i + 1}. ${a.name}`)
|
|
1107
|
+
})
|
|
812
1108
|
console.log("")
|
|
813
1109
|
|
|
814
1110
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
console.log("")
|
|
1111
|
+
|
|
1112
|
+
rl.question("Select agents (numbers separated by commas, 'all', or Enter for all): ", (answer: string) => {
|
|
1113
|
+
let selected: Agent[]
|
|
1114
|
+
|
|
1115
|
+
const trimmed = answer.trim().toLowerCase()
|
|
1116
|
+
if (trimmed === "all" || trimmed === "") {
|
|
1117
|
+
selected = agents
|
|
1118
|
+
} else if (trimmed === "none") {
|
|
1119
|
+
selected = []
|
|
1120
|
+
} else {
|
|
1121
|
+
const indices = trimmed.split(",").map(s => parseInt(s.trim(), 10) - 1).filter(i => i >= 0 && i < agents.length)
|
|
1122
|
+
selected = indices.map(i => agents[i])
|
|
828
1123
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1124
|
+
|
|
1125
|
+
if (selected.length === 0) {
|
|
1126
|
+
console.log("\nNo agents selected. Nothing installed.\n")
|
|
1127
|
+
rl.close()
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
console.log(`\nSelected: ${selected.map(a => a.name).join(", ")}\n`)
|
|
1132
|
+
|
|
1133
|
+
rl.question("Installation scope โ (1) Local or (2) Global? [1/2]: ", (scopeAnswer: string) => {
|
|
1134
|
+
const scope = scopeAnswer === "2" ? "global" : "local"
|
|
1135
|
+
console.log(`\nInstalling ${scope}ly...\n`)
|
|
1136
|
+
|
|
1137
|
+
installMemoryDir()
|
|
1138
|
+
|
|
1139
|
+
for (const agent of selected) {
|
|
1140
|
+
installForAgent(agent, scope)
|
|
1141
|
+
console.log("")
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
ensureGitignore()
|
|
1145
|
+
|
|
1146
|
+
console.log("Done! Restart your agent to use memory tools.")
|
|
1147
|
+
console.log("Run 'npx toon-memory uninstall' to remove.\n")
|
|
1148
|
+
rl.close()
|
|
1149
|
+
})
|
|
835
1150
|
})
|