toon-memory 1.7.0 → 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) {
@@ -27726,6 +27727,75 @@ function writeMemory(content) {
27726
27727
  function generateId() {
27727
27728
  return randomBytes(4).toString("hex");
27728
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
+ }
27729
27799
  function archiveOldEntries() {
27730
27800
  const data = readMemory();
27731
27801
  const lines = data.split("\n");
@@ -27742,7 +27812,10 @@ function archiveOldEntries() {
27742
27812
  const parts = line.trim().split("|");
27743
27813
  if (parts.length >= 7) {
27744
27814
  const date5 = parts[6];
27745
- if (date5 < cutoffStr) {
27815
+ const ttl = parts[7] || "";
27816
+ const isOld = date5 < cutoffStr;
27817
+ const isTtlExpired = ttl && isExpired(ttl);
27818
+ if (isOld || isTtlExpired) {
27746
27819
  toArchive.push(line.trim());
27747
27820
  } else {
27748
27821
  toKeep.push(line.trim());
@@ -27792,10 +27865,11 @@ server.registerTool(
27792
27865
  key: external_exports.string().describe("T\xEDtulo corto en kebab-case (ej: risk-engine-prioridad)"),
27793
27866
  content: external_exports.string().describe("Contenido detallado del hecho"),
27794
27867
  file: external_exports.string().optional().default("").describe("Archivo o l\xEDnea relacionada (ej: spec.md:145)"),
27795
- 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")
27796
27870
  }
27797
27871
  },
27798
- async ({ category, key, content, file: file2, tags }) => {
27872
+ async ({ category, key, content, file: file2, tags, ttl }) => {
27799
27873
  const data = readMemory();
27800
27874
  const newId = generateId();
27801
27875
  const date5 = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
@@ -27819,8 +27893,11 @@ server.registerTool(
27819
27893
  }
27820
27894
  }
27821
27895
  const entryId = existingIdx !== -1 ? existingId : newId;
27822
- 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}`;
27823
27899
  let action = "Guardado";
27900
+ const tagsInferred = !tags && resolvedTags ? true : false;
27824
27901
  if (existingIdx !== -1) {
27825
27902
  lines[existingIdx] = ` ${newEntry}`;
27826
27903
  action = "Actualizado";
@@ -27831,9 +27908,23 @@ server.registerTool(
27831
27908
  lines[headerIdx] = lines[headerIdx].replace(/\[\d+\|/, `[${count + 1}|`);
27832
27909
  }
27833
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}` : "";
27834
27925
  return {
27835
27926
  content: [{ type: "text", text: `\u{1F9E0} ${action}: ${category}/${key} (${entryId})
27836
- ${content}` }]
27927
+ ${content}${ttlMsg}${inferredMsg}${archiveMsg}` }]
27837
27928
  };
27838
27929
  }
27839
27930
  );
@@ -27858,10 +27949,11 @@ server.registerTool(
27858
27949
  const trimmed = line.trim();
27859
27950
  const parts = trimmed.split("|");
27860
27951
  if (parts.length < 7) return null;
27861
- const [id, cat, key, content, file2, tags, date5] = parts;
27952
+ const [id, cat, key, content, file2, tags, date5, ttl] = parts;
27862
27953
  if (category && cat !== category) return null;
27863
27954
  if (from_date && date5 < from_date) return null;
27864
27955
  if (to_date && date5 > to_date) return null;
27956
+ if (ttl && isExpired(ttl)) return null;
27865
27957
  const searchStr = normalize(`${id} ${cat} ${key} ${content} ${file2} ${tags}`);
27866
27958
  if (!queryTokens.every((token) => searchStr.includes(token))) return null;
27867
27959
  return { id, cat, key, content, file: file2, tags, date: date5 };
@@ -27919,12 +28011,14 @@ server.registerTool(
27919
28011
  const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"));
27920
28012
  const entries = lines.map((l) => {
27921
28013
  const parts = l.trim().split("|");
27922
- return { category: parts[1] || "unknown" };
28014
+ return { category: parts[1] || "unknown", ttl: parts[7] || "" };
27923
28015
  });
27924
28016
  const byCategory = {};
27925
28017
  for (const e of entries) {
27926
28018
  byCategory[e.category] = (byCategory[e.category] || 0) + 1;
27927
28019
  }
28020
+ const withTtl = entries.filter((e) => e.ttl).length;
28021
+ const expired = entries.filter((e) => e.ttl && isExpired(e.ttl)).length;
27928
28022
  const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries") && !/^\[\d+\|]/.test(l));
27929
28023
  const stats = [
27930
28024
  `Entradas totales: ${entries.length}`,
@@ -27933,15 +28027,67 @@ server.registerTool(
27933
28027
  "Por categor\xEDa:",
27934
28028
  ...Object.entries(byCategory).map(([k, v]) => ` ${k}: ${v}`),
27935
28029
  "",
28030
+ `TTL: ${withTtl} con expiraci\xF3n, ${expired} expiradas`,
28031
+ "",
27936
28032
  `\xDAltimas 5 entradas:`,
27937
28033
  ...lines.slice(-5).map((l) => {
27938
28034
  const parts = l.trim().split("|");
27939
- return ` [${parts[1]}] ${parts[2]} (${parts[0]})`;
28035
+ const ttlInfo = parts[7] ? ` | TTL: ${parts[7]}` : "";
28036
+ return ` [${parts[1]}] ${parts[2]} (${parts[0]})${ttlInfo}`;
27940
28037
  })
27941
28038
  ];
27942
28039
  return { content: [{ type: "text", text: stats.join("\n") }] };
27943
28040
  }
27944
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
+ );
27945
28091
  server.registerTool(
27946
28092
  "memory_summary",
27947
28093
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toon-memory",
3
- "version": "1.7.0",
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