toon-memory 1.5.0 → 1.6.2

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,9 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync } from "fs"
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, cpSync, unlinkSync, readdirSync, statSync } from "fs"
2
2
  import { basename, dirname, join } from "path"
3
3
  import { fileURLToPath } from "url"
4
4
  import { execSync } from "child_process"
5
5
  import { createInterface } from "readline"
6
6
  import { createRequire } from "module"
7
+ import { gzipSync, gunzipSync } from "zlib"
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url))
9
10
  const projectRoot = process.cwd()
@@ -12,10 +13,15 @@ const projectRoot = process.cwd()
12
13
  const sourceDir = join(__dirname, "..", "..", "src")
13
14
  const HOME = process.env.HOME || process.env.USERPROFILE || "~"
14
15
 
16
+ /** Supported AI coding agent configuration */
15
17
  interface Agent {
18
+ /** Agent identifier (e.g., "opencode", "vscode/copilot") */
16
19
  name: string
20
+ /** Global config file path (e.g., ~/.config/opencode/opencode.json) */
17
21
  global?: string
22
+ /** Local (project-level) config file path (e.g., .opencode/opencode.json) */
18
23
  local?: string
24
+ /** JSON key where MCP servers are stored */
19
25
  mcpKey: string
20
26
  }
21
27
 
@@ -27,7 +33,22 @@ try {
27
33
  execSync("npm install @toon-format/toon", { cwd: projectRoot, stdio: "inherit" })
28
34
  }
29
35
 
30
- // Detect all supported agents
36
+ /**
37
+ * Detect all supported AI coding agents on the system.
38
+ *
39
+ * Scans for configuration files in both global (~/.config/) and local
40
+ * (.opencode/, .vscode/, etc.) locations.
41
+ *
42
+ * @returns Array of detected agent configurations
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const agents = detectAgents()
47
+ * for (const agent of agents) {
48
+ * console.log(`${agent.name}: ${agent.local || "not found"}`)
49
+ * }
50
+ * ```
51
+ */
31
52
  function detectAgents(): Agent[] {
32
53
  const agents: Agent[] = []
33
54
 
@@ -94,25 +115,40 @@ function detectAgents(): Agent[] {
94
115
  return agents
95
116
  }
96
117
 
97
- // Install custom tools for OpenCode
118
+ /**
119
+ * Install memory directory for OpenCode.
120
+ *
121
+ * Creates `.opencode/memory/` directory and initial `data.toon` if needed.
122
+ * The MCP server handles all memory operations - no plugin needed.
123
+ *
124
+ * @example
125
+ * ```bash
126
+ * npx toon-memory init # Calls installOpenCodeTools()
127
+ * ```
128
+ */
98
129
  function installOpenCodeTools(): void {
99
- const toolsDir = join(projectRoot, ".opencode", "tools")
100
130
  const memoryDir = join(projectRoot, ".opencode", "memory")
101
131
  const memoryFile = join(memoryDir, "data.toon")
102
132
 
103
- if (!existsSync(toolsDir)) mkdirSync(toolsDir, { recursive: true })
104
133
  if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true })
105
134
 
106
- cpSync(join(sourceDir, "memory.ts"), join(toolsDir, "memory.ts"))
107
- console.log(" Copied memory.ts to .opencode/tools/")
108
-
109
135
  if (!existsSync(memoryFile)) {
110
- writeFileSync(memoryFile, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
136
+ writeFileSync(memoryFile, "version: 1\n[0|]\n")
111
137
  console.log(" Created .opencode/memory/data.toon")
112
138
  }
113
139
  }
114
140
 
115
- // Add .opencode/memory/ to .gitignore if not present
141
+ /**
142
+ * Add `.opencode/memory/` to `.gitignore` if not already present.
143
+ *
144
+ * Creates `.gitignore` if it doesn't exist, or appends the memory
145
+ * exclusion pattern to the existing file.
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * ensureGitignore() // Adds .opencode/memory/ to .gitignore
150
+ * ```
151
+ */
116
152
  function ensureGitignore(): void {
117
153
  const gitignorePath = join(projectRoot, ".gitignore")
118
154
  const entry = ".opencode/memory/"
@@ -130,7 +166,20 @@ function ensureGitignore(): void {
130
166
  }
131
167
  }
132
168
 
133
- // Install MCP server config for different agents
169
+ /**
170
+ * Install MCP server configuration for an agent.
171
+ *
172
+ * Adds the `toon-memory` MCP server entry to the agent's config file.
173
+ *
174
+ * @param agent - Agent configuration with config path and MCP key
175
+ * @param scope - "global" or "local" installation scope
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * const agent = { name: "opencode", local: ".opencode/opencode.json", mcpKey: "mcp" }
180
+ * installMCPConfig(agent, "local")
181
+ * ```
182
+ */
134
183
  function installMCPConfig(agent: Agent, scope: string): void {
135
184
  const configPath = scope === "global" ? agent.global : agent.local
136
185
 
@@ -154,16 +203,34 @@ function installMCPConfig(agent: Agent, scope: string): void {
154
203
  const mcpKey = agent.mcpKey || "mcpServers"
155
204
  if (!config[mcpKey]) config[mcpKey] = {}
156
205
 
157
- config[mcpKey]["toon-memory"] = {
158
- command: "npx",
159
- args: ["-y", "toon-memory", "mcp"]
206
+ // OpenCode uses a different format than other agents
207
+ if (agent.name === "opencode") {
208
+ config[mcpKey]["toon-memory"] = {
209
+ enabled: true,
210
+ type: "local",
211
+ command: ["npx", "-y", "toon-memory", "mcp"]
212
+ }
213
+ } else {
214
+ config[mcpKey]["toon-memory"] = {
215
+ command: "npx",
216
+ args: ["-y", "toon-memory", "mcp"]
217
+ }
160
218
  }
161
219
 
162
220
  writeFileSync(configPath, JSON.stringify(config, null, 2))
163
221
  console.log(` MCP server added to ${configPath}`)
164
222
  }
165
223
 
166
- // Uninstall from all agents
224
+ /**
225
+ * Uninstall toon-memory from all detected agents.
226
+ *
227
+ * Removes MCP server configurations and custom tools from all agents.
228
+ *
229
+ * @example
230
+ * ```bash
231
+ * npx toon-memory uninstall
232
+ * ```
233
+ */
167
234
  function uninstall(): void {
168
235
  console.log("\n🧠 toon-memory uninstaller\n")
169
236
 
@@ -198,7 +265,20 @@ function uninstall(): void {
198
265
  console.log("\nāœ… toon-memory uninstalled from all agents\n")
199
266
  }
200
267
 
201
- // Quick init without interactive prompts
268
+ /**
269
+ * Initialize toon-memory for all detected agents (non-interactive).
270
+ *
271
+ * Installs MCP server configs, creates memory directory, and
272
+ * updates `.gitignore`.
273
+ *
274
+ * @param scope - "local" (default) or "global" installation scope
275
+ *
276
+ * @example
277
+ * ```bash
278
+ * npx toon-memory init # Local install
279
+ * npx toon-memory init global # Global install
280
+ * ```
281
+ */
202
282
  function init(scope: string = "local"): void {
203
283
  console.log("\n🧠 toon-memory init\n")
204
284
 
@@ -220,7 +300,16 @@ function init(scope: string = "local"): void {
220
300
  console.log("Done! Restart your agent to use memory tools.\n")
221
301
  }
222
302
 
223
- // Show installation status
303
+ /**
304
+ * Show toon-memory installation status.
305
+ *
306
+ * Displays version, memory entry count, and agent configuration status.
307
+ *
308
+ * @example
309
+ * ```bash
310
+ * npx toon-memory status
311
+ * ```
312
+ */
224
313
  function status(): void {
225
314
  console.log("\n🧠 toon-memory status\n")
226
315
 
@@ -272,7 +361,16 @@ function status(): void {
272
361
  console.log("")
273
362
  }
274
363
 
275
- // Upgrade to latest version
364
+ /**
365
+ * Upgrade toon-memory to the latest version.
366
+ *
367
+ * Checks npm registry for updates and installs if available.
368
+ *
369
+ * @example
370
+ * ```bash
371
+ * npx toon-memory upgrade
372
+ * ```
373
+ */
276
374
  function upgrade(): void {
277
375
  console.log("\n🧠 toon-memory upgrade\n")
278
376
 
@@ -291,7 +389,16 @@ function upgrade(): void {
291
389
  }
292
390
  }
293
391
 
294
- // Show memory statistics
392
+ /**
393
+ * Display memory statistics.
394
+ *
395
+ * Shows entry counts by category, last update date, and file size.
396
+ *
397
+ * @example
398
+ * ```bash
399
+ * npx toon-memory stats
400
+ * ```
401
+ */
295
402
  function stats(): void {
296
403
  console.log("\n🧠 toon-memory stats\n")
297
404
 
@@ -333,7 +440,17 @@ function stats(): void {
333
440
  console.log("")
334
441
  }
335
442
 
336
- // Export memory to JSON
443
+ /**
444
+ * Export memory to JSON format.
445
+ *
446
+ * Creates a `toon-memory-export.json` file with all entries
447
+ * for backup or transfer to another project.
448
+ *
449
+ * @example
450
+ * ```bash
451
+ * npx toon-memory export
452
+ * ```
453
+ */
337
454
  function exportMemory(): void {
338
455
  console.log("\n🧠 toon-memory export\n")
339
456
 
@@ -374,7 +491,18 @@ function exportMemory(): void {
374
491
  console.log(` ${outputPath}\n`)
375
492
  }
376
493
 
377
- // Import memory from JSON
494
+ /**
495
+ * Import memory from JSON file.
496
+ *
497
+ * Imports entries from a JSON export, skipping duplicates based on key.
498
+ *
499
+ * @param file - Path to JSON file (relative or absolute)
500
+ *
501
+ * @example
502
+ * ```bash
503
+ * npx toon-memory import toon-memory-export.json
504
+ * ```
505
+ */
378
506
  function importMemory(): void {
379
507
  console.log("\n🧠 toon-memory import\n")
380
508
 
@@ -446,7 +574,104 @@ function importMemory(): void {
446
574
  console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates\n`)
447
575
  }
448
576
 
449
- // Watch mode - backup memory every N minutes
577
+ /** Watch mode options */
578
+ interface WatchOptions {
579
+ /** Backup interval in minutes (default: 5) */
580
+ interval: number
581
+ /** Maximum number of backups to keep (0 = unlimited) */
582
+ maxBackups: number
583
+ /** Enable gzip compression for backups */
584
+ compress: boolean
585
+ /** Enable file logging */
586
+ logFile: boolean
587
+ /** Log file path */
588
+ logPath: string
589
+ }
590
+
591
+ /** Parse watch CLI arguments into WatchOptions */
592
+ function parseWatchOptions(args: string[]): WatchOptions {
593
+ const opts: WatchOptions = {
594
+ interval: 5,
595
+ maxBackups: 10,
596
+ compress: false,
597
+ logFile: false,
598
+ logPath: ""
599
+ }
600
+
601
+ for (let i = 1; i < args.length; i++) {
602
+ const arg = args[i]
603
+ if (arg === "--compress" || arg === "-c") {
604
+ opts.compress = true
605
+ } else if (arg === "--log" || arg === "-l") {
606
+ opts.logFile = true
607
+ opts.logPath = args[++i] || join(projectRoot, ".opencode", "memory", "watch.log")
608
+ } else if (arg === "--max-backups" || arg === "-m") {
609
+ opts.maxBackups = parseInt(args[++i]) || 10
610
+ } else if (!arg.startsWith("-")) {
611
+ opts.interval = parseInt(arg) || 5
612
+ }
613
+ }
614
+
615
+ return opts
616
+ }
617
+
618
+ /** Write a line to the watch log file */
619
+ function writeWatchLog(logPath: string, message: string): void {
620
+ if (!logPath) return
621
+ const timestamp = new Date().toISOString()
622
+ const logLine = `[${timestamp}] ${message}\n`
623
+ writeFileSync(logPath, logLine, { flag: "a" })
624
+ }
625
+
626
+ /** Get list of backup files sorted by creation time (oldest first) */
627
+ function getBackupFiles(backupDir: string): string[] {
628
+ if (!existsSync(backupDir)) return []
629
+
630
+ return readdirSync(backupDir)
631
+ .filter(f => f.startsWith("backup-") && (f.endsWith(".toon") || f.endsWith(".gz")))
632
+ .map(f => join(backupDir, f))
633
+ .sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs)
634
+ }
635
+
636
+ /** Remove oldest backups if we exceed maxBackups */
637
+ function pruneBackups(backupDir: string, maxBackups: number): number {
638
+ if (maxBackups <= 0) return 0
639
+
640
+ const files = getBackupFiles(backupDir)
641
+ const excess = files.length - maxBackups
642
+
643
+ if (excess <= 0) return 0
644
+
645
+ for (let i = 0; i < excess; i++) {
646
+ unlinkSync(files[i])
647
+ }
648
+
649
+ return excess
650
+ }
651
+
652
+ /** Compress content with gzip */
653
+ function compressData(data: string): Buffer {
654
+ return gzipSync(Buffer.from(data, "utf-8"))
655
+ }
656
+
657
+ /** Decompress gzip data to string */
658
+ function decompressData(data: Buffer): string {
659
+ return gunzipSync(data).toString("utf-8")
660
+ }
661
+
662
+ /**
663
+ * Watch mode - backup memory every N minutes
664
+ *
665
+ * @example
666
+ * ```bash
667
+ * npx toon-memory watch # Default: 5 min interval, 10 max backups
668
+ * npx toon-memory watch 10 # 10 minute interval
669
+ * npx toon-memory watch -c # Enable compression
670
+ * npx toon-memory watch -l # Enable file logging
671
+ * npx toon-memory watch -m 20 # Keep max 20 backups
672
+ * npx toon-memory watch 15 -c -l -m 5 # All options
673
+ * ```
674
+ */
450
675
  function watch(): void {
451
676
  console.log("\n🧠 toon-memory watch\n")
452
677
 
@@ -460,36 +685,83 @@ function watch(): void {
460
685
 
461
686
  if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true })
462
687
 
463
- const intervalMinutes = parseInt(args[1]) || 5
464
- console.log(`Watching memory file every ${intervalMinutes} minutes...`)
688
+ const opts = parseWatchOptions(args)
689
+
690
+ if (opts.logFile) {
691
+ const logDir = dirname(opts.logPath)
692
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
693
+ }
694
+
695
+ console.log(`Watching memory file every ${opts.interval} minutes...`)
696
+ console.log(`Max backups: ${opts.maxBackups === 0 ? "unlimited" : opts.maxBackups}`)
697
+ console.log(`Compression: ${opts.compress ? "enabled" : "disabled"}`)
698
+ console.log(`Logging: ${opts.logFile ? `enabled (${opts.logPath})` : "disabled"}`)
465
699
  console.log(`Press Ctrl+C to stop\n`)
466
700
 
467
701
  let lastContent = readFileSync(memoryFile, "utf-8")
702
+ let lastHash = hashContent(lastContent)
468
703
  let backupCount = 0
469
704
 
705
+ if (opts.logFile) writeWatchLog(opts.logPath, "Watch started")
706
+
470
707
  const backup = () => {
471
- const currentContent = readFileSync(memoryFile, "utf-8")
472
-
473
- if (currentContent !== lastContent) {
474
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
475
- const backupFile = join(backupDir, `backup-${timestamp}.toon`)
476
- writeFileSync(backupFile, currentContent)
477
- backupCount++
478
- console.log(`šŸ“¦ Backup #${backupCount} created: ${timestamp}`)
479
- lastContent = currentContent
708
+ try {
709
+ const currentContent = readFileSync(memoryFile, "utf-8")
710
+ const currentHash = hashContent(currentContent)
711
+
712
+ if (currentHash !== lastHash) {
713
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
714
+ const ext = opts.compress ? ".toon.gz" : ".toon"
715
+ const backupFile = join(backupDir, `backup-${timestamp}${ext}`)
716
+
717
+ if (opts.compress) {
718
+ writeFileSync(backupFile, compressData(currentContent))
719
+ } else {
720
+ writeFileSync(backupFile, currentContent)
721
+ }
722
+
723
+ backupCount++
724
+ console.log(`šŸ“¦ Backup #${backupCount} created: ${timestamp}`)
725
+ if (opts.logFile) writeWatchLog(opts.logPath, `Backup #${backupCount}: ${timestamp}`)
726
+
727
+ lastContent = currentContent
728
+ lastHash = currentHash
729
+
730
+ const pruned = pruneBackups(backupDir, opts.maxBackups)
731
+ if (pruned > 0) {
732
+ console.log(`šŸ—‘ļø Pruned ${pruned} old backup(s)`)
733
+ if (opts.logFile) writeWatchLog(opts.logPath, `Pruned ${pruned} old backup(s)`)
734
+ }
735
+ }
736
+ } catch (err) {
737
+ const msg = `Error creating backup: ${(err as Error).message}`
738
+ console.error(`āŒ ${msg}`)
739
+ if (opts.logFile) writeWatchLog(opts.logPath, msg)
740
+ }
741
+ }
742
+
743
+ /** Simple hash for change detection (not cryptographic) */
744
+ function hashContent(content: string): string {
745
+ let hash = 0
746
+ for (let i = 0; i < content.length; i++) {
747
+ const char = content.charCodeAt(i)
748
+ hash = ((hash << 5) - hash) + char
749
+ hash = hash & hash
480
750
  }
751
+ return hash.toString(36)
481
752
  }
482
753
 
483
754
  // Initial backup
484
755
  backup()
485
756
 
486
757
  // Set interval
487
- const interval = setInterval(backup, intervalMinutes * 60 * 1000)
758
+ const interval = setInterval(backup, opts.interval * 60 * 1000)
488
759
 
489
760
  // Handle Ctrl+C
490
761
  process.on("SIGINT", () => {
491
762
  clearInterval(interval)
492
763
  console.log(`\nāœ… Watch stopped. ${backupCount} backups created.\n`)
764
+ if (opts.logFile) writeWatchLog(opts.logPath, `Watch stopped. ${backupCount} backups created.`)
493
765
  process.exit(0)
494
766
  })
495
767