toon-memory 1.4.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,7 +115,17 @@ function detectAgents(): Agent[] {
94
115
  return agents
95
116
  }
96
117
 
97
- // Install custom tools for OpenCode
118
+ /**
119
+ * Install custom tools and memory directory for OpenCode.
120
+ *
121
+ * Creates `.opencode/tools/` and `.opencode/memory/` directories,
122
+ * copies `memory.ts` tool, and creates initial `data.toon` if needed.
123
+ *
124
+ * @example
125
+ * ```bash
126
+ * npx toon-memory init # Calls installOpenCodeTools()
127
+ * ```
128
+ */
98
129
  function installOpenCodeTools(): void {
99
130
  const toolsDir = join(projectRoot, ".opencode", "tools")
100
131
  const memoryDir = join(projectRoot, ".opencode", "memory")
@@ -112,7 +143,17 @@ function installOpenCodeTools(): void {
112
143
  }
113
144
  }
114
145
 
115
- // Add .opencode/memory/ to .gitignore if not present
146
+ /**
147
+ * Add `.opencode/memory/` to `.gitignore` if not already present.
148
+ *
149
+ * Creates `.gitignore` if it doesn't exist, or appends the memory
150
+ * exclusion pattern to the existing file.
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * ensureGitignore() // Adds .opencode/memory/ to .gitignore
155
+ * ```
156
+ */
116
157
  function ensureGitignore(): void {
117
158
  const gitignorePath = join(projectRoot, ".gitignore")
118
159
  const entry = ".opencode/memory/"
@@ -130,7 +171,20 @@ function ensureGitignore(): void {
130
171
  }
131
172
  }
132
173
 
133
- // Install MCP server config for different agents
174
+ /**
175
+ * Install MCP server configuration for an agent.
176
+ *
177
+ * Adds the `toon-memory` MCP server entry to the agent's config file.
178
+ *
179
+ * @param agent - Agent configuration with config path and MCP key
180
+ * @param scope - "global" or "local" installation scope
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * const agent = { name: "opencode", local: ".opencode/opencode.json", mcpKey: "mcp" }
185
+ * installMCPConfig(agent, "local")
186
+ * ```
187
+ */
134
188
  function installMCPConfig(agent: Agent, scope: string): void {
135
189
  const configPath = scope === "global" ? agent.global : agent.local
136
190
 
@@ -154,16 +208,34 @@ function installMCPConfig(agent: Agent, scope: string): void {
154
208
  const mcpKey = agent.mcpKey || "mcpServers"
155
209
  if (!config[mcpKey]) config[mcpKey] = {}
156
210
 
157
- config[mcpKey]["toon-memory"] = {
158
- command: "npx",
159
- args: ["-y", "toon-memory", "mcp"]
211
+ // OpenCode uses a different format than other agents
212
+ if (agent.name === "opencode") {
213
+ config[mcpKey]["toon-memory"] = {
214
+ enabled: true,
215
+ type: "local",
216
+ command: ["npx", "-y", "toon-memory", "mcp"]
217
+ }
218
+ } else {
219
+ config[mcpKey]["toon-memory"] = {
220
+ command: "npx",
221
+ args: ["-y", "toon-memory", "mcp"]
222
+ }
160
223
  }
161
224
 
162
225
  writeFileSync(configPath, JSON.stringify(config, null, 2))
163
226
  console.log(` MCP server added to ${configPath}`)
164
227
  }
165
228
 
166
- // Uninstall from all agents
229
+ /**
230
+ * Uninstall toon-memory from all detected agents.
231
+ *
232
+ * Removes MCP server configurations and custom tools from all agents.
233
+ *
234
+ * @example
235
+ * ```bash
236
+ * npx toon-memory uninstall
237
+ * ```
238
+ */
167
239
  function uninstall(): void {
168
240
  console.log("\n🧠 toon-memory uninstaller\n")
169
241
 
@@ -198,7 +270,20 @@ function uninstall(): void {
198
270
  console.log("\nāœ… toon-memory uninstalled from all agents\n")
199
271
  }
200
272
 
201
- // Quick init without interactive prompts
273
+ /**
274
+ * Initialize toon-memory for all detected agents (non-interactive).
275
+ *
276
+ * Installs MCP server configs, creates memory directory, and
277
+ * updates `.gitignore`.
278
+ *
279
+ * @param scope - "local" (default) or "global" installation scope
280
+ *
281
+ * @example
282
+ * ```bash
283
+ * npx toon-memory init # Local install
284
+ * npx toon-memory init global # Global install
285
+ * ```
286
+ */
202
287
  function init(scope: string = "local"): void {
203
288
  console.log("\n🧠 toon-memory init\n")
204
289
 
@@ -220,7 +305,16 @@ function init(scope: string = "local"): void {
220
305
  console.log("Done! Restart your agent to use memory tools.\n")
221
306
  }
222
307
 
223
- // Show installation status
308
+ /**
309
+ * Show toon-memory installation status.
310
+ *
311
+ * Displays version, memory entry count, and agent configuration status.
312
+ *
313
+ * @example
314
+ * ```bash
315
+ * npx toon-memory status
316
+ * ```
317
+ */
224
318
  function status(): void {
225
319
  console.log("\n🧠 toon-memory status\n")
226
320
 
@@ -272,7 +366,16 @@ function status(): void {
272
366
  console.log("")
273
367
  }
274
368
 
275
- // Upgrade to latest version
369
+ /**
370
+ * Upgrade toon-memory to the latest version.
371
+ *
372
+ * Checks npm registry for updates and installs if available.
373
+ *
374
+ * @example
375
+ * ```bash
376
+ * npx toon-memory upgrade
377
+ * ```
378
+ */
276
379
  function upgrade(): void {
277
380
  console.log("\n🧠 toon-memory upgrade\n")
278
381
 
@@ -291,7 +394,16 @@ function upgrade(): void {
291
394
  }
292
395
  }
293
396
 
294
- // Show memory statistics
397
+ /**
398
+ * Display memory statistics.
399
+ *
400
+ * Shows entry counts by category, last update date, and file size.
401
+ *
402
+ * @example
403
+ * ```bash
404
+ * npx toon-memory stats
405
+ * ```
406
+ */
295
407
  function stats(): void {
296
408
  console.log("\n🧠 toon-memory stats\n")
297
409
 
@@ -333,7 +445,17 @@ function stats(): void {
333
445
  console.log("")
334
446
  }
335
447
 
336
- // Export memory to JSON
448
+ /**
449
+ * Export memory to JSON format.
450
+ *
451
+ * Creates a `toon-memory-export.json` file with all entries
452
+ * for backup or transfer to another project.
453
+ *
454
+ * @example
455
+ * ```bash
456
+ * npx toon-memory export
457
+ * ```
458
+ */
337
459
  function exportMemory(): void {
338
460
  console.log("\n🧠 toon-memory export\n")
339
461
 
@@ -374,7 +496,18 @@ function exportMemory(): void {
374
496
  console.log(` ${outputPath}\n`)
375
497
  }
376
498
 
377
- // Import memory from JSON
499
+ /**
500
+ * Import memory from JSON file.
501
+ *
502
+ * Imports entries from a JSON export, skipping duplicates based on key.
503
+ *
504
+ * @param file - Path to JSON file (relative or absolute)
505
+ *
506
+ * @example
507
+ * ```bash
508
+ * npx toon-memory import toon-memory-export.json
509
+ * ```
510
+ */
378
511
  function importMemory(): void {
379
512
  console.log("\n🧠 toon-memory import\n")
380
513
 
@@ -446,6 +579,203 @@ function importMemory(): void {
446
579
  console.log(`Skipped ${importData.entries.length - newEntries.length} duplicates\n`)
447
580
  }
448
581
 
582
+ /** Watch mode options */
583
+ interface WatchOptions {
584
+ /** Backup interval in minutes (default: 5) */
585
+ interval: number
586
+ /** Maximum number of backups to keep (0 = unlimited) */
587
+ maxBackups: number
588
+ /** Enable gzip compression for backups */
589
+ compress: boolean
590
+ /** Enable file logging */
591
+ logFile: boolean
592
+ /** Log file path */
593
+ logPath: string
594
+ }
595
+
596
+ /** Parse watch CLI arguments into WatchOptions */
597
+ function parseWatchOptions(args: string[]): WatchOptions {
598
+ const opts: WatchOptions = {
599
+ interval: 5,
600
+ maxBackups: 10,
601
+ compress: false,
602
+ logFile: false,
603
+ logPath: ""
604
+ }
605
+
606
+ for (let i = 1; i < args.length; i++) {
607
+ const arg = args[i]
608
+ if (arg === "--compress" || arg === "-c") {
609
+ opts.compress = true
610
+ } else if (arg === "--log" || arg === "-l") {
611
+ opts.logFile = true
612
+ opts.logPath = args[++i] || join(projectRoot, ".opencode", "memory", "watch.log")
613
+ } else if (arg === "--max-backups" || arg === "-m") {
614
+ opts.maxBackups = parseInt(args[++i]) || 10
615
+ } else if (!arg.startsWith("-")) {
616
+ opts.interval = parseInt(arg) || 5
617
+ }
618
+ }
619
+
620
+ return opts
621
+ }
622
+
623
+ /** Write a line to the watch log file */
624
+ function writeWatchLog(logPath: string, message: string): void {
625
+ if (!logPath) return
626
+ const timestamp = new Date().toISOString()
627
+ const logLine = `[${timestamp}] ${message}\n`
628
+ writeFileSync(logPath, logLine, { flag: "a" })
629
+ }
630
+
631
+ /** Get list of backup files sorted by creation time (oldest first) */
632
+ function getBackupFiles(backupDir: string): string[] {
633
+ if (!existsSync(backupDir)) return []
634
+
635
+ return readdirSync(backupDir)
636
+ .filter(f => f.startsWith("backup-") && (f.endsWith(".toon") || f.endsWith(".gz")))
637
+ .map(f => join(backupDir, f))
638
+ .sort((a, b) => statSync(a).mtimeMs - statSync(b).mtimeMs)
639
+ }
640
+
641
+ /** Remove oldest backups if we exceed maxBackups */
642
+ function pruneBackups(backupDir: string, maxBackups: number): number {
643
+ if (maxBackups <= 0) return 0
644
+
645
+ const files = getBackupFiles(backupDir)
646
+ const excess = files.length - maxBackups
647
+
648
+ if (excess <= 0) return 0
649
+
650
+ for (let i = 0; i < excess; i++) {
651
+ unlinkSync(files[i])
652
+ }
653
+
654
+ return excess
655
+ }
656
+
657
+ /** Compress content with gzip */
658
+ function compressData(data: string): Buffer {
659
+ return gzipSync(Buffer.from(data, "utf-8"))
660
+ }
661
+
662
+ /** Decompress gzip data to string */
663
+ function decompressData(data: Buffer): string {
664
+ return gunzipSync(data).toString("utf-8")
665
+ }
666
+
667
+ /**
668
+ * Watch mode - backup memory every N minutes
669
+ *
670
+ * @example
671
+ * ```bash
672
+ * npx toon-memory watch # Default: 5 min interval, 10 max backups
673
+ * npx toon-memory watch 10 # 10 minute interval
674
+ * npx toon-memory watch -c # Enable compression
675
+ * npx toon-memory watch -l # Enable file logging
676
+ * npx toon-memory watch -m 20 # Keep max 20 backups
677
+ * npx toon-memory watch 15 -c -l -m 5 # All options
678
+ * ```
679
+ */
680
+ function watch(): void {
681
+ console.log("\n🧠 toon-memory watch\n")
682
+
683
+ const memoryFile = join(projectRoot, ".opencode", "memory", "data.toon")
684
+ const backupDir = join(projectRoot, ".opencode", "memory", "backups")
685
+
686
+ if (!existsSync(memoryFile)) {
687
+ console.log("Memory not initialized. Run 'npx toon-memory init' first.\n")
688
+ return
689
+ }
690
+
691
+ if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true })
692
+
693
+ const opts = parseWatchOptions(args)
694
+
695
+ if (opts.logFile) {
696
+ const logDir = dirname(opts.logPath)
697
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true })
698
+ }
699
+
700
+ console.log(`Watching memory file every ${opts.interval} minutes...`)
701
+ console.log(`Max backups: ${opts.maxBackups === 0 ? "unlimited" : opts.maxBackups}`)
702
+ console.log(`Compression: ${opts.compress ? "enabled" : "disabled"}`)
703
+ console.log(`Logging: ${opts.logFile ? `enabled (${opts.logPath})` : "disabled"}`)
704
+ console.log(`Press Ctrl+C to stop\n`)
705
+
706
+ let lastContent = readFileSync(memoryFile, "utf-8")
707
+ let lastHash = hashContent(lastContent)
708
+ let backupCount = 0
709
+
710
+ if (opts.logFile) writeWatchLog(opts.logPath, "Watch started")
711
+
712
+ const backup = () => {
713
+ try {
714
+ const currentContent = readFileSync(memoryFile, "utf-8")
715
+ const currentHash = hashContent(currentContent)
716
+
717
+ if (currentHash !== lastHash) {
718
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
719
+ const ext = opts.compress ? ".toon.gz" : ".toon"
720
+ const backupFile = join(backupDir, `backup-${timestamp}${ext}`)
721
+
722
+ if (opts.compress) {
723
+ writeFileSync(backupFile, compressData(currentContent))
724
+ } else {
725
+ writeFileSync(backupFile, currentContent)
726
+ }
727
+
728
+ backupCount++
729
+ console.log(`šŸ“¦ Backup #${backupCount} created: ${timestamp}`)
730
+ if (opts.logFile) writeWatchLog(opts.logPath, `Backup #${backupCount}: ${timestamp}`)
731
+
732
+ lastContent = currentContent
733
+ lastHash = currentHash
734
+
735
+ const pruned = pruneBackups(backupDir, opts.maxBackups)
736
+ if (pruned > 0) {
737
+ console.log(`šŸ—‘ļø Pruned ${pruned} old backup(s)`)
738
+ if (opts.logFile) writeWatchLog(opts.logPath, `Pruned ${pruned} old backup(s)`)
739
+ }
740
+ }
741
+ } catch (err) {
742
+ const msg = `Error creating backup: ${(err as Error).message}`
743
+ console.error(`āŒ ${msg}`)
744
+ if (opts.logFile) writeWatchLog(opts.logPath, msg)
745
+ }
746
+ }
747
+
748
+ /** Simple hash for change detection (not cryptographic) */
749
+ function hashContent(content: string): string {
750
+ let hash = 0
751
+ for (let i = 0; i < content.length; i++) {
752
+ const char = content.charCodeAt(i)
753
+ hash = ((hash << 5) - hash) + char
754
+ hash = hash & hash
755
+ }
756
+ return hash.toString(36)
757
+ }
758
+
759
+ // Initial backup
760
+ backup()
761
+
762
+ // Set interval
763
+ const interval = setInterval(backup, opts.interval * 60 * 1000)
764
+
765
+ // Handle Ctrl+C
766
+ process.on("SIGINT", () => {
767
+ clearInterval(interval)
768
+ console.log(`\nāœ… Watch stopped. ${backupCount} backups created.\n`)
769
+ if (opts.logFile) writeWatchLog(opts.logPath, `Watch stopped. ${backupCount} backups created.`)
770
+ process.exit(0)
771
+ })
772
+
773
+ process.on("SIGTERM", () => {
774
+ clearInterval(interval)
775
+ process.exit(0)
776
+ })
777
+ }
778
+
449
779
  // Main
450
780
  const args = process.argv.slice(2)
451
781
 
@@ -484,6 +814,11 @@ if (args[0] === "import") {
484
814
  process.exit(0)
485
815
  }
486
816
 
817
+ if (args[0] === "watch") {
818
+ watch()
819
+ process.exit(0)
820
+ }
821
+
487
822
  const agents = detectAgents()
488
823
  console.log("\n🧠 toon-memory installer\n")
489
824