toon-memory 1.6.9 → 1.8.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/mcp/server.js CHANGED
@@ -27644,10 +27644,11 @@ import { dirname, join } from "path";
27644
27644
  import { fileURLToPath } from "url";
27645
27645
  import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
27646
27646
  var __dirname = dirname(fileURLToPath(import.meta.url));
27647
- var MEMORY_DIR = join(process.cwd(), ".opencode", "memory");
27647
+ var MEMORY_DIR = join(process.cwd(), ".toon-memory", "memory");
27648
27648
  var MEMORY_FILE = join(MEMORY_DIR, "data.toon");
27649
27649
  var ARCHIVE_FILE = join(MEMORY_DIR, "archive.toon");
27650
27650
  var CONFIG_FILE = join(MEMORY_DIR, "config.json");
27651
+ var MAX_ENTRIES = 100;
27651
27652
  var ARCHIVE_DAYS = 30;
27652
27653
  var ALGORITHM = "aes-256-gcm";
27653
27654
  function loadConfig() {
@@ -27673,7 +27674,7 @@ function ensureMemoryDir() {
27673
27674
  function ensureMemoryFile() {
27674
27675
  ensureMemoryDir();
27675
27676
  if (!existsSync(MEMORY_FILE)) {
27676
- writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n");
27677
+ writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date|ttl}:\n");
27677
27678
  }
27678
27679
  }
27679
27680
  function encrypt(text, key) {
@@ -27696,9 +27697,6 @@ function decrypt(encryptedData, key) {
27696
27697
  decrypted += decipher.final("utf8");
27697
27698
  return decrypted;
27698
27699
  }
27699
- function generateKey() {
27700
- return randomBytes(32).toString("hex");
27701
- }
27702
27700
  function readMemory() {
27703
27701
  ensureMemoryFile();
27704
27702
  const config2 = loadConfig();
@@ -27729,6 +27727,75 @@ function writeMemory(content) {
27729
27727
  function generateId() {
27730
27728
  return randomBytes(4).toString("hex");
27731
27729
  }
27730
+ function parseTTL(ttl) {
27731
+ if (!ttl || !ttl.trim()) return "";
27732
+ const trimmed = ttl.trim();
27733
+ const dayMatch = trimmed.match(/^(\d+)d$/);
27734
+ if (dayMatch) {
27735
+ const days = parseInt(dayMatch[1]);
27736
+ const d = /* @__PURE__ */ new Date();
27737
+ d.setDate(d.getDate() + days);
27738
+ return d.toISOString().split("T")[0];
27739
+ }
27740
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed;
27741
+ return "";
27742
+ }
27743
+ function isExpired(ttl) {
27744
+ if (!ttl) return false;
27745
+ const ttlDate = parseTTL(ttl) || ttl;
27746
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27747
+ return ttlDate <= today;
27748
+ }
27749
+ var TAG_VOCABULARY = {
27750
+ "redis": ["redis", "cache", "caching", "memcached"],
27751
+ "auth": ["auth", "authentication", "authorization", "login", "token", "jwt", "session", "oauth"],
27752
+ "api": ["api", "endpoint", "rest", "graphql", "route", "router", "controller"],
27753
+ "db": ["database", "db", "sql", "postgres", "mysql", "mongo", "query", "migration", "schema"],
27754
+ "security": ["security", "encrypt", "decrypt", "vulnerability", "xss", "csrf", "cors", "sanitiz"],
27755
+ "test": ["test", "testing", "vitest", "jest", "spec", "mock", "assert", "coverage"],
27756
+ "deploy": ["deploy", "docker", "ci/cd", "github actions", "pipeline", "kubernetes", "k8s"],
27757
+ "config": ["config", "configuration", "settings", "env", "environment", "dotenv"],
27758
+ "performance": ["performance", "optimize", "benchmark", "latency", "throughput", "cache"],
27759
+ "refactor": ["refactor", "cleanup", "restructure", "reorganize", "rework"],
27760
+ "error": ["error", "exception", "throw", "catch", "handling", "retry", "fallback"],
27761
+ "logging": ["log", "logging", "logger", "debug", "trace", "monitor", "observability"],
27762
+ "types": ["typescript", "types", "type", "interface", "generic", "enum", "zod", "schema"],
27763
+ "async": ["async", "await", "promise", "concurrent", "parallel", "worker", "queue"],
27764
+ "state": ["state", "store", "redux", "context", "reducer", "action", "observable"],
27765
+ "ui": ["ui", "component", "render", "dom", "css", "style", "layout", "responsive"],
27766
+ "storage": ["storage", "file", "filesystem", "s3", "blob", "upload", "download"],
27767
+ "email": ["email", "mail", "smtp", "sendgrid", "newsletter", "notification"],
27768
+ "payment": ["payment", "stripe", "billing", "invoice", "checkout", "subscription"],
27769
+ "webhook": ["webhook", "callback", "event", "listener", "hook"]
27770
+ };
27771
+ function inferTags(content, key) {
27772
+ const text = `${key} ${content}`.toLowerCase();
27773
+ const matched = [];
27774
+ for (const [tag, keywords] of Object.entries(TAG_VOCABULARY)) {
27775
+ if (keywords.some((kw) => text.includes(kw))) {
27776
+ matched.push(tag);
27777
+ }
27778
+ }
27779
+ return matched.join(";");
27780
+ }
27781
+ function parseRelativeDate(since) {
27782
+ const trimmed = since.trim();
27783
+ const hourMatch = trimmed.match(/^(\d+)h$/);
27784
+ if (hourMatch) {
27785
+ const d = /* @__PURE__ */ new Date();
27786
+ d.setHours(d.getHours() - parseInt(hourMatch[1]));
27787
+ return d.toISOString().split("T")[0];
27788
+ }
27789
+ const dayMatch = trimmed.match(/^(\d+)d$/);
27790
+ if (dayMatch) {
27791
+ const d = /* @__PURE__ */ new Date();
27792
+ d.setDate(d.getDate() - parseInt(dayMatch[1]));
27793
+ return d.toISOString().split("T")[0];
27794
+ }
27795
+ if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed;
27796
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
27797
+ return today;
27798
+ }
27732
27799
  function archiveOldEntries() {
27733
27800
  const data = readMemory();
27734
27801
  const lines = data.split("\n");
@@ -27745,7 +27812,10 @@ function archiveOldEntries() {
27745
27812
  const parts = line.trim().split("|");
27746
27813
  if (parts.length >= 7) {
27747
27814
  const date5 = parts[6];
27748
- if (date5 < cutoffStr) {
27815
+ const ttl = parts[7] || "";
27816
+ const isOld = date5 < cutoffStr;
27817
+ const isTtlExpired = ttl && isExpired(ttl);
27818
+ if (isOld || isTtlExpired) {
27749
27819
  toArchive.push(line.trim());
27750
27820
  } else {
27751
27821
  toKeep.push(line.trim());
@@ -27795,10 +27865,11 @@ server.registerTool(
27795
27865
  key: external_exports.string().describe("T\xEDtulo corto en kebab-case (ej: risk-engine-prioridad)"),
27796
27866
  content: external_exports.string().describe("Contenido detallado del hecho"),
27797
27867
  file: external_exports.string().optional().default("").describe("Archivo o l\xEDnea relacionada (ej: spec.md:145)"),
27798
- tags: external_exports.string().optional().default("").describe("Tags separados por punto y coma (ej: risk;spec)")
27868
+ tags: external_exports.string().optional().default("").describe("Tags separados por punto y coma (ej: risk;spec)"),
27869
+ ttl: external_exports.string().optional().default("").describe("Time to live (ej: 7d, 2026-07-17). Vac\xEDo = sin expiraci\xF3n")
27799
27870
  }
27800
27871
  },
27801
- async ({ category, key, content, file: file2, tags }) => {
27872
+ async ({ category, key, content, file: file2, tags, ttl }) => {
27802
27873
  const data = readMemory();
27803
27874
  const newId = generateId();
27804
27875
  const date5 = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -27822,8 +27893,11 @@ server.registerTool(
27822
27893
  }
27823
27894
  }
27824
27895
  const entryId = existingIdx !== -1 ? existingId : newId;
27825
- const newEntry = `${entryId}|${category}|${key}|${content}|${file2 || ""}|${tags || ""}|${date5}`;
27896
+ const resolvedTtl = parseTTL(ttl);
27897
+ const resolvedTags = tags ? tags : inferTags(content, key);
27898
+ const newEntry = `${entryId}|${category}|${key}|${content}|${file2 || ""}|${resolvedTags}|${date5}|${resolvedTtl}`;
27826
27899
  let action = "Guardado";
27900
+ const tagsInferred = !tags && resolvedTags ? true : false;
27827
27901
  if (existingIdx !== -1) {
27828
27902
  lines[existingIdx] = ` ${newEntry}`;
27829
27903
  action = "Actualizado";
@@ -27834,9 +27908,23 @@ server.registerTool(
27834
27908
  lines[headerIdx] = lines[headerIdx].replace(/\[\d+\|/, `[${count + 1}|`);
27835
27909
  }
27836
27910
  writeMemory(lines.join("\n"));
27911
+ const headerMatch = lines[headerIdx].match(/\[(\d+)\|/);
27912
+ const entryCount = headerMatch ? parseInt(headerMatch[1]) : 0;
27913
+ let archiveMsg = "";
27914
+ if (entryCount > MAX_ENTRIES) {
27915
+ const result = archiveOldEntries();
27916
+ if (result.archived > 0) {
27917
+ archiveMsg = `
27918
+ \u{1F4E6} Auto-archived ${result.archived} old entries (${result.kept} kept)`;
27919
+ }
27920
+ }
27921
+ const ttlMsg = resolvedTtl ? `
27922
+ \u23F0 TTL: ${resolvedTtl}` : "";
27923
+ const inferredMsg = tagsInferred ? `
27924
+ \u{1F3F7}\uFE0F Tags inferidos: ${resolvedTags}` : "";
27837
27925
  return {
27838
27926
  content: [{ type: "text", text: `\u{1F9E0} ${action}: ${category}/${key} (${entryId})
27839
- ${content}` }]
27927
+ ${content}${ttlMsg}${inferredMsg}${archiveMsg}` }]
27840
27928
  };
27841
27929
  }
27842
27930
  );
@@ -27861,10 +27949,11 @@ server.registerTool(
27861
27949
  const trimmed = line.trim();
27862
27950
  const parts = trimmed.split("|");
27863
27951
  if (parts.length < 7) return null;
27864
- const [id, cat, key, content, file2, tags, date5] = parts;
27952
+ const [id, cat, key, content, file2, tags, date5, ttl] = parts;
27865
27953
  if (category && cat !== category) return null;
27866
27954
  if (from_date && date5 < from_date) return null;
27867
27955
  if (to_date && date5 > to_date) return null;
27956
+ if (ttl && isExpired(ttl)) return null;
27868
27957
  const searchStr = normalize(`${id} ${cat} ${key} ${content} ${file2} ${tags}`);
27869
27958
  if (!queryTokens.every((token) => searchStr.includes(token))) return null;
27870
27959
  return { id, cat, key, content, file: file2, tags, date: date5 };
@@ -27922,12 +28011,14 @@ server.registerTool(
27922
28011
  const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"));
27923
28012
  const entries = lines.map((l) => {
27924
28013
  const parts = l.trim().split("|");
27925
- return { category: parts[1] || "unknown" };
28014
+ return { category: parts[1] || "unknown", ttl: parts[7] || "" };
27926
28015
  });
27927
28016
  const byCategory = {};
27928
28017
  for (const e of entries) {
27929
28018
  byCategory[e.category] = (byCategory[e.category] || 0) + 1;
27930
28019
  }
28020
+ const withTtl = entries.filter((e) => e.ttl).length;
28021
+ const expired = entries.filter((e) => e.ttl && isExpired(e.ttl)).length;
27931
28022
  const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries") && !/^\[\d+\|]/.test(l));
27932
28023
  const stats = [
27933
28024
  `Entradas totales: ${entries.length}`,
@@ -27936,15 +28027,67 @@ server.registerTool(
27936
28027
  "Por categor\xEDa:",
27937
28028
  ...Object.entries(byCategory).map(([k, v]) => ` ${k}: ${v}`),
27938
28029
  "",
28030
+ `TTL: ${withTtl} con expiraci\xF3n, ${expired} expiradas`,
28031
+ "",
27939
28032
  `\xDAltimas 5 entradas:`,
27940
28033
  ...lines.slice(-5).map((l) => {
27941
28034
  const parts = l.trim().split("|");
27942
- return ` [${parts[1]}] ${parts[2]} (${parts[0]})`;
28035
+ const ttlInfo = parts[7] ? ` | TTL: ${parts[7]}` : "";
28036
+ return ` [${parts[1]}] ${parts[2]} (${parts[0]})${ttlInfo}`;
27943
28037
  })
27944
28038
  ];
27945
28039
  return { content: [{ type: "text", text: stats.join("\n") }] };
27946
28040
  }
27947
28041
  );
28042
+ server.registerTool(
28043
+ "memory_diff",
28044
+ {
28045
+ title: "Memory Diff",
28046
+ description: "Muestra qu\xE9 cambi\xF3 en la memoria desde una fecha. \xDAtil para saber qu\xE9 se aprendi\xF3 desde la \xFAltima sesi\xF3n.",
28047
+ inputSchema: {
28048
+ since: external_exports.string().describe("Desde cu\xE1ndo mostrar cambios (ej: 24h, 7d, 2026-07-10)"),
28049
+ type: external_exports.enum(["all", "created", "updated"]).optional().default("all").describe("Filtrar por tipo de cambio")
28050
+ }
28051
+ },
28052
+ async ({ since, type }) => {
28053
+ const sinceDate = parseRelativeDate(since);
28054
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
28055
+ const data = readMemory();
28056
+ const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"));
28057
+ const results = lines.map((line) => {
28058
+ const trimmed = line.trim();
28059
+ const parts = trimmed.split("|");
28060
+ if (parts.length < 7) return null;
28061
+ const [id, cat, key, content, file2, tags, date5] = parts;
28062
+ if (date5 < sinceDate) return null;
28063
+ const changeType = date5 === today ? "created" : "updated";
28064
+ if (type !== "all" && changeType !== type) return null;
28065
+ return { id, cat, key, content, file: file2, tags, date: date5, changeType };
28066
+ }).filter(Boolean);
28067
+ if (results.length === 0) {
28068
+ return { content: [{ type: "text", text: `No hay cambios desde ${sinceDate}` }] };
28069
+ }
28070
+ const created = results.filter((r) => r.changeType === "created");
28071
+ const updated = results.filter((r) => r.changeType === "updated");
28072
+ const sections = [`\u{1F4CB} Cambios desde ${sinceDate}:`, ""];
28073
+ if (created.length > 0 && (type === "all" || type === "created")) {
28074
+ sections.push(`\u2795 Nuevas (${created.length}):`);
28075
+ for (const r of created) {
28076
+ sections.push(` [${r.cat}] ${r.key} (${r.id})
28077
+ ${r.content}`);
28078
+ }
28079
+ sections.push("");
28080
+ }
28081
+ if (updated.length > 0 && (type === "all" || type === "updated")) {
28082
+ sections.push(`\u270F\uFE0F Actualizadas (${updated.length}):`);
28083
+ for (const r of updated) {
28084
+ sections.push(` [${r.cat}] ${r.key} (${r.id}) \u2014 ${r.date}`);
28085
+ }
28086
+ sections.push("");
28087
+ }
28088
+ return { content: [{ type: "text", text: sections.join("\n") }] };
28089
+ }
28090
+ );
27948
28091
  server.registerTool(
27949
28092
  "memory_summary",
27950
28093
  {
@@ -28025,18 +28168,16 @@ server.registerTool(
28025
28168
  if (config2.encrypted) {
28026
28169
  return { content: [{ type: "text", text: "La encriptaci\xF3n ya est\xE1 habilitada" }] };
28027
28170
  }
28028
- const key = generateKey();
28171
+ const key = getKey();
28172
+ if (!key) {
28173
+ return { content: [{ type: "text", text: "\u274C Define TOON_MEMORY_KEY en el entorno antes de encriptar" }] };
28174
+ }
28029
28175
  const data = readFileSync(MEMORY_FILE, "utf-8");
28030
28176
  const encrypted = encrypt(data, key);
28031
28177
  writeFileSync(MEMORY_FILE, encrypted);
28032
28178
  saveConfig({ encrypted: true });
28033
28179
  return {
28034
- content: [{
28035
- type: "text",
28036
- text: `\u{1F510} Encriptaci\xF3n habilitada
28037
- \u26A0\uFE0F Guarda esta clave en TOON_MEMORY_KEY (no se puede recuperar):
28038
- ${key}`
28039
- }]
28180
+ content: [{ type: "text", text: "\u{1F510} Encriptaci\xF3n habilitada" }]
28040
28181
  };
28041
28182
  }
28042
28183
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toon-memory",
3
- "version": "1.6.9",
3
+ "version": "1.8.0",
4
4
  "description": "Persistent memory system for AI coding agents using TOON format (40% fewer tokens than JSON)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,6 +45,14 @@
45
45
  "windsurf",
46
46
  "cline",
47
47
  "continue",
48
+ "codex",
49
+ "gemini",
50
+ "zed",
51
+ "antigravity",
52
+ "aider",
53
+ "kilocode",
54
+ "openclaw",
55
+ "kiro",
48
56
  "ai-agent",
49
57
  "memory",
50
58
  "toon",
@@ -6,16 +6,23 @@ A persistent memory system for AI coding agents. It remembers decisions, pattern
6
6
 
7
7
  ## Supported Agents
8
8
 
9
- | Agent | Config File | Format |
10
- |-------|-------------|--------|
11
- | OpenCode | `.opencode/opencode.json` | MCP server |
12
- | VS Code / Copilot | `.vscode/mcp.json` | MCP server |
13
- | Claude | `.claude/settings.json` | MCP server |
14
- | Cursor | `.cursor/mcp.json` | MCP server |
15
- | Windsurf | `.windsurfrules` | MCP server |
16
- | Cline | `.cline/mcp.json` | MCP server |
17
- | Continue | `.continue/config.json` | MCP server |
18
- | Aider | `.aider.conf.yml` | Manual config |
9
+ | Agent | Config File | Format | Hooks |
10
+ |-------|-------------|--------|-------|
11
+ | OpenCode | `.opencode/opencode.json` | MCP server | — |
12
+ | VS Code / Copilot | `.vscode/mcp.json` | MCP server | — |
13
+ | Claude Code | `.claude/settings.json` | MCP server | SessionStart |
14
+ | Cursor | `.cursor/mcp.json` | MCP server | — |
15
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` | MCP server | — |
16
+ | Cline | `.cline/mcp.json` | MCP server | — |
17
+ | Continue | `.continue/config.json` | MCP server | — |
18
+ | Codex CLI | `.codex/config.toml` | TOML | SessionStart |
19
+ | Gemini CLI | `.gemini/settings.json` | MCP server | SessionStart |
20
+ | Zed | `~/.config/zed/settings.json` | JSONC | — |
21
+ | Antigravity | `.gemini/config/mcp_config.json` | MCP server | SessionStart |
22
+ | Aider | — | Instructions only | — |
23
+ | KiloCode | `~/.kilocode/mcp_settings.json` | MCP server | — |
24
+ | OpenClaw | `.openclaw.json` | MCP server | — |
25
+ | Kiro | `.kiro/settings/mcp.json` | MCP server | — |
19
26
 
20
27
  ## Tools
21
28
 
@@ -26,6 +33,9 @@ A persistent memory system for AI coding agents. It remembers decisions, pattern
26
33
  | `memory_forget` | Remove an entry by key or id |
27
34
  | `memory_stats` | View memory state |
28
35
  | `memory_summary` | Save/retrieve file summaries |
36
+ | `memory_archive` | Archive old entries |
37
+ | `memory_encrypt` | Enable encryption |
38
+ | `memory_decrypt` | Disable encryption |
29
39
 
30
40
  ## Installation
31
41
 
@@ -37,8 +47,9 @@ npx toon-memory
37
47
 
38
48
  This will:
39
49
  1. Detect installed agents
40
- 2. Ask if you want local or global installation
41
- 3. Configure the MCP server automatically
50
+ 2. Let you select which agents to configure
51
+ 3. Ask for local or global installation
52
+ 4. Configure MCP server, instructions, and hooks automatically
42
53
 
43
54
  ### Manual installation
44
55
 
@@ -71,7 +82,7 @@ Add to your agent's MCP config:
71
82
  ### When exploring the project
72
83
 
73
84
  1. First: `memory_recall(query="what you need to know")`
74
- 2. If no result: use CodeGraph or grep
85
+ 2. If no result: use grep or search tools
75
86
  3. Only if both fail: read files directly
76
87
 
77
88
  ### At the END of every session
@@ -91,7 +102,7 @@ Add to your agent's MCP config:
91
102
 
92
103
  ## File format
93
104
 
94
- Data is stored in `.opencode/memory/data.toon` using TOON (Token-Oriented Object Notation):
105
+ Data is stored in `.toon-memory/memory/data.toon` using TOON (Token-Oriented Object Notation):
95
106
 
96
107
  ```
97
108
  version: 1