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/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
- const opencodeGlobal = join(HOME, ".config", "opencode", "opencode.json")
47
- const opencodeLocal = join(projectRoot, ".opencode", "opencode.json")
48
- agents.push({
49
- name: "opencode",
50
- global: opencodeGlobal,
51
- local: opencodeLocal,
52
- mcpKey: "mcp"
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
- const vscodeLocal = join(projectRoot, ".vscode", "mcp.json")
57
- agents.push({
58
- name: "vscode/copilot",
59
- local: vscodeLocal,
60
- mcpKey: "servers"
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
- const claudeGlobal = join(HOME, ".claude", "settings.json")
65
- const claudeLocal = join(projectRoot, ".claude", "settings.json")
66
- agents.push({
67
- name: "claude",
68
- global: claudeGlobal,
69
- local: claudeLocal,
70
- mcpKey: "mcpServers"
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
- const cursorLocal = join(projectRoot, ".cursor", "mcp.json")
75
- agents.push({
76
- name: "cursor",
77
- local: cursorLocal,
78
- mcpKey: "mcpServers"
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
- const windsurfGlobal = join(HOME, ".codeium", "windsurf", "mcp_config.json")
83
- agents.push({
84
- name: "windsurf",
85
- global: windsurfGlobal,
86
- mcpKey: "mcpServers"
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
- const clineLocal = join(projectRoot, ".cline", "mcp.json")
91
- agents.push({
92
- name: "cline",
93
- local: clineLocal,
94
- mcpKey: "mcpServers"
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
- const continueLocal = join(projectRoot, ".continue", "config.json")
99
- agents.push({
100
- name: "continue",
101
- local: continueLocal,
102
- mcpKey: "mcpServers"
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 for OpenCode.
110
- *
111
- * Creates `.opencode/memory/` directory and initial `data.toon` if needed.
112
- * The MCP server handles all memory operations - no plugin needed.
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 installOpenCodeTools(): void {
120
- const memoryDir = join(projectRoot, ".opencode", "memory")
121
- const memoryFile = join(memoryDir, "data.toon")
232
+ function installMemoryDir(): void {
233
+ const memoryFile = join(MEMORY_DIR, "data.toon")
122
234
 
123
- if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true })
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 .opencode/memory/data.toon")
239
+ console.log(" Created .toon-memory/memory/data.toon")
128
240
  }
129
241
  }
130
242
 
131
243
  /**
132
- * Add `.opencode/memory/` to `.gitignore` if not already present.
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 = ".opencode/memory/"
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 .opencode/memory/ to .gitignore")
259
+ console.log(" Added .toon-memory/memory/ to .gitignore")
156
260
  }
157
261
  }
158
262
 
159
263
  /**
160
- * Install MCP server configuration for an agent.
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 installMCPConfig(agent: Agent, scope: string): void {
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
- * Uninstall toon-memory from all detected agents.
216
- *
217
- * Removes MCP server configurations and custom tools from all agents.
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 uninstall(): void {
225
- console.log("\n๐Ÿง  toon-memory uninstaller\n")
226
-
227
- const agents = detectAgents()
228
-
229
- for (const agent of agents) {
230
- const configs = [agent.global, agent.local].filter(Boolean) as string[]
231
-
232
- for (const configPath of configs) {
233
- if (!existsSync(configPath)) continue
234
-
235
- try {
236
- const config = JSON.parse(readFileSync(configPath, "utf-8"))
237
- const mcpKey = agent.mcpKey || "mcpServers"
238
-
239
- if (config[mcpKey]?.["toon-memory"]) {
240
- delete config[mcpKey]["toon-memory"]
241
- writeFileSync(configPath, JSON.stringify(config, null, 2))
242
- console.log(` โœ… Removed from ${agent.name} (${configPath})`)
243
- }
244
- } catch {}
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
- // Remove custom tools
249
- const toolsFile = join(projectRoot, ".opencode", "tools", "memory.ts")
250
- if (existsSync(toolsFile)) {
251
- unlinkSync(toolsFile)
252
- console.log(" โœ… Removed .opencode/tools/memory.ts")
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
- console.log("\nโœ… toon-memory uninstalled from all agents\n")
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
- console.log(`${agent.name}:`)
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(projectRoot, ".opencode", "memory", "data.toon")
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
- const configs = [agent.global, agent.local].filter(Boolean) as string[]
330
- let found = false
331
-
332
- for (const configPath of configs) {
333
- if (!existsSync(configPath)) continue
334
-
335
- try {
336
- const config = JSON.parse(readFileSync(configPath, "utf-8"))
337
- const mcpKey = agent.mcpKey || "mcpServers"
338
-
339
- if (config[mcpKey]?.["toon-memory"]) {
340
- console.log(` โœ… ${agent.name} (${configPath})`)
341
- found = true
342
- }
343
- } catch {}
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
- if (!found) {
347
- console.log(` โŒ ${agent.name} (not configured)`)
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(projectRoot, ".opencode", "memory", "data.toon")
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(projectRoot, ".opencode", "memory", "data.toon")
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 memoryDir = join(projectRoot, ".opencode", "memory")
528
- const memoryFile = join(memoryDir, "data.toon")
529
-
530
- if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true })
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(projectRoot, ".opencode", "memory", "watch.log")
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(projectRoot, ".opencode", "memory", "data.toon")
669
- const backupDir = join(projectRoot, ".opencode", "memory", "backups")
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("Supported agents:")
811
- agents.forEach((a: Agent, i: number) => console.log(` ${i + 1}. ${a.name}`))
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
- rl.question("Install (1) Local or (2) Global? [1/2]: ", (answer: string) => {
816
- const scope = answer === "2" ? "global" : "local"
817
- console.log(`\nInstalling ${scope}ly...\n`)
818
-
819
- for (const agent of agents) {
820
- console.log(`${agent.name}:`)
821
-
822
- if (agent.name === "opencode") {
823
- installOpenCodeTools()
824
- }
825
-
826
- installMCPConfig(agent, scope)
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
- ensureGitignore()
831
-
832
- console.log("Done! Restart your agent to use memory tools.")
833
- console.log("Run 'npx toon-memory uninstall' to remove.\n")
834
- rl.close()
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
  })