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/README.md +270 -34
- package/dist/cli/setup.js +409 -96
- package/mcp/server.js +268 -19
- package/package.json +9 -1
- package/skills/toon-memory.md +25 -14
- package/src/cli/setup.ts +650 -335
- package/src/mcp/server.ts +327 -11
- package/uninstall.sh +33 -5
package/src/mcp/server.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { randomBytes, createCipheriv, createDecipheriv } from "crypto"
|
|
|
9
9
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
|
|
11
11
|
/** Base directory for memory storage */
|
|
12
|
-
const MEMORY_DIR = join(process.cwd(), ".
|
|
12
|
+
const MEMORY_DIR = join(process.cwd(), ".toon-memory", "memory")
|
|
13
13
|
|
|
14
14
|
/** Main memory data file */
|
|
15
15
|
const MEMORY_FILE = join(MEMORY_DIR, "data.toon")
|
|
@@ -91,7 +91,7 @@ function ensureMemoryDir(): void {
|
|
|
91
91
|
function ensureMemoryFile(): void {
|
|
92
92
|
ensureMemoryDir()
|
|
93
93
|
if (!existsSync(MEMORY_FILE)) {
|
|
94
|
-
writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date}:\n")
|
|
94
|
+
writeFileSync(MEMORY_FILE, "version: 1\nentries[0|]{id|category|key|content|file|tags|date|ttl}:\n")
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
@@ -207,6 +207,130 @@ function generateId(): string {
|
|
|
207
207
|
return randomBytes(4).toString("hex")
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Parse a TTL value into an absolute date string (YYYY-MM-DD).
|
|
212
|
+
* Supports: exact dates (2026-07-17), relative days (7d, 30d).
|
|
213
|
+
* Returns empty string if no TTL.
|
|
214
|
+
*/
|
|
215
|
+
function parseTTL(ttl: string): string {
|
|
216
|
+
if (!ttl || !ttl.trim()) return ""
|
|
217
|
+
const trimmed = ttl.trim()
|
|
218
|
+
const dayMatch = trimmed.match(/^(\d+)d$/)
|
|
219
|
+
if (dayMatch) {
|
|
220
|
+
const days = parseInt(dayMatch[1])
|
|
221
|
+
const d = new Date()
|
|
222
|
+
d.setDate(d.getDate() + days)
|
|
223
|
+
return d.toISOString().split("T")[0]
|
|
224
|
+
}
|
|
225
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed
|
|
226
|
+
return ""
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if a TTL value has expired (date is in the past).
|
|
231
|
+
*/
|
|
232
|
+
function isExpired(ttl: string): boolean {
|
|
233
|
+
if (!ttl) return false
|
|
234
|
+
const ttlDate = parseTTL(ttl) || ttl
|
|
235
|
+
const today = new Date().toISOString().split("T")[0]
|
|
236
|
+
return ttlDate <= today
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Built-in vocabulary for automatic tag inference */
|
|
240
|
+
const TAG_VOCABULARY: Record<string, string[]> = {
|
|
241
|
+
"redis": ["redis", "cache", "caching", "memcached"],
|
|
242
|
+
"auth": ["auth", "authentication", "authorization", "login", "token", "jwt", "session", "oauth"],
|
|
243
|
+
"api": ["api", "endpoint", "rest", "graphql", "route", "router", "controller"],
|
|
244
|
+
"db": ["database", "db", "sql", "postgres", "mysql", "mongo", "query", "migration", "schema"],
|
|
245
|
+
"security": ["security", "encrypt", "decrypt", "vulnerability", "xss", "csrf", "cors", "sanitiz"],
|
|
246
|
+
"test": ["test", "testing", "vitest", "jest", "spec", "mock", "assert", "coverage"],
|
|
247
|
+
"deploy": ["deploy", "docker", "ci/cd", "github actions", "pipeline", "kubernetes", "k8s"],
|
|
248
|
+
"config": ["config", "configuration", "settings", "env", "environment", "dotenv"],
|
|
249
|
+
"performance": ["performance", "optimize", "benchmark", "latency", "throughput", "cache"],
|
|
250
|
+
"refactor": ["refactor", "cleanup", "restructure", "reorganize", "rework"],
|
|
251
|
+
"error": ["error", "exception", "throw", "catch", "handling", "retry", "fallback"],
|
|
252
|
+
"logging": ["log", "logging", "logger", "debug", "trace", "monitor", "observability"],
|
|
253
|
+
"types": ["typescript", "types", "type", "interface", "generic", "enum", "zod", "schema"],
|
|
254
|
+
"async": ["async", "await", "promise", "concurrent", "parallel", "worker", "queue"],
|
|
255
|
+
"state": ["state", "store", "redux", "context", "reducer", "action", "observable"],
|
|
256
|
+
"ui": ["ui", "component", "render", "dom", "css", "style", "layout", "responsive"],
|
|
257
|
+
"storage": ["storage", "file", "filesystem", "s3", "blob", "upload", "download"],
|
|
258
|
+
"email": ["email", "mail", "smtp", "sendgrid", "newsletter", "notification"],
|
|
259
|
+
"payment": ["payment", "stripe", "billing", "invoice", "checkout", "subscription"],
|
|
260
|
+
"webhook": ["webhook", "callback", "event", "listener", "hook"],
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Infer tags from content and key by matching against vocabulary.
|
|
265
|
+
* Returns semicolon-separated tags.
|
|
266
|
+
*/
|
|
267
|
+
function inferTags(content: string, key: string): string {
|
|
268
|
+
const text = `${key} ${content}`.toLowerCase()
|
|
269
|
+
const matched: string[] = []
|
|
270
|
+
for (const [tag, keywords] of Object.entries(TAG_VOCABULARY)) {
|
|
271
|
+
if (keywords.some((kw) => text.includes(kw))) {
|
|
272
|
+
matched.push(tag)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return matched.join(";")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Parse a relative date string into an absolute YYYY-MM-DD date.
|
|
280
|
+
* Supports: "24h", "7d", "30d", or an exact date "2026-07-10".
|
|
281
|
+
*/
|
|
282
|
+
function parseRelativeDate(since: string): string {
|
|
283
|
+
const trimmed = since.trim()
|
|
284
|
+
const hourMatch = trimmed.match(/^(\d+)h$/)
|
|
285
|
+
if (hourMatch) {
|
|
286
|
+
const d = new Date()
|
|
287
|
+
d.setHours(d.getHours() - parseInt(hourMatch[1]))
|
|
288
|
+
return d.toISOString().split("T")[0]
|
|
289
|
+
}
|
|
290
|
+
const dayMatch = trimmed.match(/^(\d+)d$/)
|
|
291
|
+
if (dayMatch) {
|
|
292
|
+
const d = new Date()
|
|
293
|
+
d.setDate(d.getDate() - parseInt(dayMatch[1]))
|
|
294
|
+
return d.toISOString().split("T")[0]
|
|
295
|
+
}
|
|
296
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return trimmed
|
|
297
|
+
const today = new Date().toISOString().split("T")[0]
|
|
298
|
+
return today
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const normalize = (s: string) => s.toLowerCase().replace(/[-_]/g, " ").replace(/\s+/g, " ").trim()
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Find entries related to the given text by fuzzy matching.
|
|
305
|
+
* Returns top N results ranked by match quality.
|
|
306
|
+
*/
|
|
307
|
+
function findRelatedEntries(text: string, excludeKey: string = "", limit: number = 3): Array<{ id: string; cat: string; key: string; content: string; file: string; tags: string; date: string; score: number }> {
|
|
308
|
+
const data = readMemory()
|
|
309
|
+
const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"))
|
|
310
|
+
const queryTokens = normalize(text).split(" ").filter(Boolean)
|
|
311
|
+
|
|
312
|
+
const scored = lines
|
|
313
|
+
.map((line) => {
|
|
314
|
+
const trimmed = line.trim()
|
|
315
|
+
const parts = trimmed.split("|")
|
|
316
|
+
if (parts.length < 7) return null
|
|
317
|
+
const [id, cat, key, content, file, tags, date] = parts
|
|
318
|
+
if (key === excludeKey) return null
|
|
319
|
+
const searchStr = normalize(`${id} ${cat} ${key} ${content} ${file} ${tags}`)
|
|
320
|
+
let score = 0
|
|
321
|
+
for (const token of queryTokens) {
|
|
322
|
+
if (searchStr.includes(token)) score++
|
|
323
|
+
}
|
|
324
|
+
if (score === 0) return null
|
|
325
|
+
return { id, cat, key, content, file, tags, date, score }
|
|
326
|
+
})
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
.sort((a, b) => b!.score - a!.score)
|
|
329
|
+
.slice(0, limit)
|
|
330
|
+
|
|
331
|
+
return scored as Array<{ id: string; cat: string; key: string; content: string; file: string; tags: string; date: string; score: number }>
|
|
332
|
+
}
|
|
333
|
+
|
|
210
334
|
/**
|
|
211
335
|
* Archive entries older than ARCHIVE_DAYS to archive.toon.
|
|
212
336
|
*
|
|
@@ -241,7 +365,10 @@ function archiveOldEntries(): { archived: number; kept: number } {
|
|
|
241
365
|
const parts = line.trim().split("|")
|
|
242
366
|
if (parts.length >= 7) {
|
|
243
367
|
const date = parts[6]
|
|
244
|
-
|
|
368
|
+
const ttl = parts[7] || ""
|
|
369
|
+
const isOld = date < cutoffStr
|
|
370
|
+
const isTtlExpired = ttl && isExpired(ttl)
|
|
371
|
+
if (isOld || isTtlExpired) {
|
|
245
372
|
toArchive.push(line.trim())
|
|
246
373
|
} else {
|
|
247
374
|
toKeep.push(line.trim())
|
|
@@ -287,8 +414,71 @@ function archiveOldEntries(): { archived: number; kept: number } {
|
|
|
287
414
|
}
|
|
288
415
|
|
|
289
416
|
const server = new McpServer(
|
|
290
|
-
{ name: "toon-memory", version: "
|
|
291
|
-
{ capabilities: { tools: { listChanged: true } } }
|
|
417
|
+
{ name: "toon-memory", version: "2.0.0" },
|
|
418
|
+
{ capabilities: { tools: { listChanged: true }, resources: { listChanged: true } } }
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Register memory entries as an MCP resource.
|
|
423
|
+
* Agents can read this as context in the system prompt.
|
|
424
|
+
*/
|
|
425
|
+
server.registerResource(
|
|
426
|
+
"memory-entries",
|
|
427
|
+
"toon://memory/entries",
|
|
428
|
+
{ title: "Memory Entries", mimeType: "text/plain" },
|
|
429
|
+
async (uri) => {
|
|
430
|
+
const data = readMemory()
|
|
431
|
+
return { contents: [{ uri: uri.href, text: data }] }
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Register memory stats as an MCP resource.
|
|
437
|
+
*/
|
|
438
|
+
server.registerResource(
|
|
439
|
+
"memory-stats",
|
|
440
|
+
"toon://memory/stats",
|
|
441
|
+
{ title: "Memory Stats", mimeType: "text/plain" },
|
|
442
|
+
async (uri) => {
|
|
443
|
+
const data = readMemory()
|
|
444
|
+
const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"))
|
|
445
|
+
const entries = lines.map((l) => {
|
|
446
|
+
const parts = l.trim().split("|")
|
|
447
|
+
return { category: parts[1] || "unknown", ttl: parts[7] || "" }
|
|
448
|
+
})
|
|
449
|
+
const byCategory: Record<string, number> = {}
|
|
450
|
+
for (const e of entries) {
|
|
451
|
+
byCategory[e.category] = (byCategory[e.category] || 0) + 1
|
|
452
|
+
}
|
|
453
|
+
const withTtl = entries.filter((e) => e.ttl).length
|
|
454
|
+
const expired = entries.filter((e) => e.ttl && isExpired(e.ttl)).length
|
|
455
|
+
const text = [
|
|
456
|
+
`Entradas totales: ${entries.length}`,
|
|
457
|
+
`TTL: ${withTtl} con expiración, ${expired} expiradas`,
|
|
458
|
+
"Por categoría:",
|
|
459
|
+
...Object.entries(byCategory).map(([k, v]) => ` ${k}: ${v}`),
|
|
460
|
+
].join("\n")
|
|
461
|
+
return { contents: [{ uri: uri.href, text }] }
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Register memory summaries as an MCP resource.
|
|
467
|
+
*/
|
|
468
|
+
server.registerResource(
|
|
469
|
+
"memory-summaries",
|
|
470
|
+
"toon://memory/summaries",
|
|
471
|
+
{ title: "Memory Summaries", mimeType: "text/plain" },
|
|
472
|
+
async (uri) => {
|
|
473
|
+
const data = readMemory()
|
|
474
|
+
const lines = data.split("\n")
|
|
475
|
+
const summaryIdx = lines.findIndex((l) => l.trim().startsWith("summaries:"))
|
|
476
|
+
if (summaryIdx === -1) {
|
|
477
|
+
return { contents: [{ uri: uri.href, text: "No hay resúmenes guardados" }] }
|
|
478
|
+
}
|
|
479
|
+
const summaryText = lines.slice(summaryIdx + 1).filter((l) => l.includes(":")).join("\n")
|
|
480
|
+
return { contents: [{ uri: uri.href, text: summaryText || "No hay resúmenes guardados" }] }
|
|
481
|
+
}
|
|
292
482
|
)
|
|
293
483
|
|
|
294
484
|
/**
|
|
@@ -316,9 +506,10 @@ server.registerTool(
|
|
|
316
506
|
content: z.string().describe("Contenido detallado del hecho"),
|
|
317
507
|
file: z.string().optional().default("").describe("Archivo o línea relacionada (ej: spec.md:145)"),
|
|
318
508
|
tags: z.string().optional().default("").describe("Tags separados por punto y coma (ej: risk;spec)"),
|
|
509
|
+
ttl: z.string().optional().default("").describe("Time to live (ej: 7d, 2026-07-17). Vacío = sin expiración"),
|
|
319
510
|
},
|
|
320
511
|
},
|
|
321
|
-
async ({ category, key, content, file, tags }) => {
|
|
512
|
+
async ({ category, key, content, file, tags, ttl }) => {
|
|
322
513
|
const data = readMemory()
|
|
323
514
|
const newId = generateId()
|
|
324
515
|
const date = new Date().toISOString().split("T")[0]
|
|
@@ -346,8 +537,11 @@ server.registerTool(
|
|
|
346
537
|
}
|
|
347
538
|
|
|
348
539
|
const entryId = existingIdx !== -1 ? existingId : newId
|
|
349
|
-
const
|
|
540
|
+
const resolvedTtl = parseTTL(ttl)
|
|
541
|
+
const resolvedTags = tags ? tags : inferTags(content, key)
|
|
542
|
+
const newEntry = `${entryId}|${category}|${key}|${content}|${file || ""}|${resolvedTags}|${date}|${resolvedTtl}`
|
|
350
543
|
let action = "Guardado"
|
|
544
|
+
const tagsInferred = !tags && resolvedTags ? true : false
|
|
351
545
|
|
|
352
546
|
if (existingIdx !== -1) {
|
|
353
547
|
// Update existing entry
|
|
@@ -362,8 +556,30 @@ server.registerTool(
|
|
|
362
556
|
}
|
|
363
557
|
|
|
364
558
|
writeMemory(lines.join("\n"))
|
|
559
|
+
|
|
560
|
+
// Auto-archive if we exceed MAX_ENTRIES
|
|
561
|
+
const headerMatch = lines[headerIdx].match(/\[(\d+)\|/)
|
|
562
|
+
const entryCount = headerMatch ? parseInt(headerMatch[1]) : 0
|
|
563
|
+
let archiveMsg = ""
|
|
564
|
+
if (entryCount > MAX_ENTRIES) {
|
|
565
|
+
const result = archiveOldEntries()
|
|
566
|
+
if (result.archived > 0) {
|
|
567
|
+
archiveMsg = `\n📦 Auto-archived ${result.archived} old entries (${result.kept} kept)`
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const ttlMsg = resolvedTtl ? `\n⏰ TTL: ${resolvedTtl}` : ""
|
|
572
|
+
const inferredMsg = tagsInferred ? `\n🏷️ Tags inferidos: ${resolvedTags}` : ""
|
|
573
|
+
|
|
574
|
+
const related = findRelatedEntries(`${key} ${content} ${resolvedTags}`, key, 3)
|
|
575
|
+
let relatedMsg = ""
|
|
576
|
+
if (related.length > 0) {
|
|
577
|
+
const items = related.map((r) => ` [${r.cat}] ${r.key} — ${r.content.slice(0, 80)}`).join("\n")
|
|
578
|
+
relatedMsg = `\n\n🔗 Entradas relacionadas:\n${items}`
|
|
579
|
+
}
|
|
580
|
+
|
|
365
581
|
return {
|
|
366
|
-
content: [{ type: "text" as const, text: `🧠 ${action}: ${category}/${key} (${entryId})\n${content}` }],
|
|
582
|
+
content: [{ type: "text" as const, text: `🧠 ${action}: ${category}/${key} (${entryId})\n${content}${ttlMsg}${inferredMsg}${archiveMsg}${relatedMsg}` }],
|
|
367
583
|
}
|
|
368
584
|
}
|
|
369
585
|
)
|
|
@@ -404,10 +620,11 @@ server.registerTool(
|
|
|
404
620
|
const trimmed = line.trim()
|
|
405
621
|
const parts = trimmed.split("|")
|
|
406
622
|
if (parts.length < 7) return null
|
|
407
|
-
const [id, cat, key, content, file, tags, date] = parts
|
|
623
|
+
const [id, cat, key, content, file, tags, date, ttl] = parts
|
|
408
624
|
if (category && cat !== category) return null
|
|
409
625
|
if (from_date && date < from_date) return null
|
|
410
626
|
if (to_date && date > to_date) return null
|
|
627
|
+
if (ttl && isExpired(ttl)) return null
|
|
411
628
|
const searchStr = normalize(`${id} ${cat} ${key} ${content} ${file} ${tags}`)
|
|
412
629
|
// All query tokens must match (AND logic)
|
|
413
630
|
if (!queryTokens.every((token) => searchStr.includes(token))) return null
|
|
@@ -494,7 +711,7 @@ server.registerTool(
|
|
|
494
711
|
const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"))
|
|
495
712
|
const entries = lines.map((l) => {
|
|
496
713
|
const parts = l.trim().split("|")
|
|
497
|
-
return { category: parts[1] || "unknown" }
|
|
714
|
+
return { category: parts[1] || "unknown", ttl: parts[7] || "" }
|
|
498
715
|
})
|
|
499
716
|
|
|
500
717
|
const byCategory: Record<string, number> = {}
|
|
@@ -502,6 +719,9 @@ server.registerTool(
|
|
|
502
719
|
byCategory[e.category] = (byCategory[e.category] || 0) + 1
|
|
503
720
|
}
|
|
504
721
|
|
|
722
|
+
const withTtl = entries.filter((e) => e.ttl).length
|
|
723
|
+
const expired = entries.filter((e) => e.ttl && isExpired(e.ttl)).length
|
|
724
|
+
|
|
505
725
|
const summaryLines = data.split("\n").filter((l) => l.includes(":") && !l.startsWith(" ") && !l.startsWith("version") && !l.startsWith("entries") && !/^\[\d+\|]/.test(l))
|
|
506
726
|
const stats = [
|
|
507
727
|
`Entradas totales: ${entries.length}`,
|
|
@@ -510,10 +730,13 @@ server.registerTool(
|
|
|
510
730
|
"Por categoría:",
|
|
511
731
|
...Object.entries(byCategory).map(([k, v]) => ` ${k}: ${v}`),
|
|
512
732
|
"",
|
|
733
|
+
`TTL: ${withTtl} con expiración, ${expired} expiradas`,
|
|
734
|
+
"",
|
|
513
735
|
`Últimas 5 entradas:`,
|
|
514
736
|
...lines.slice(-5).map((l) => {
|
|
515
737
|
const parts = l.trim().split("|")
|
|
516
|
-
|
|
738
|
+
const ttlInfo = parts[7] ? ` | TTL: ${parts[7]}` : ""
|
|
739
|
+
return ` [${parts[1]}] ${parts[2]} (${parts[0]})${ttlInfo}`
|
|
517
740
|
}),
|
|
518
741
|
]
|
|
519
742
|
|
|
@@ -521,6 +744,99 @@ server.registerTool(
|
|
|
521
744
|
}
|
|
522
745
|
)
|
|
523
746
|
|
|
747
|
+
/**
|
|
748
|
+
* Register the memory_diff tool.
|
|
749
|
+
* Show what changed in memory since a given date.
|
|
750
|
+
*/
|
|
751
|
+
server.registerTool(
|
|
752
|
+
"memory_diff",
|
|
753
|
+
{
|
|
754
|
+
title: "Memory Diff",
|
|
755
|
+
description: "Muestra qué cambió en la memoria desde una fecha. Útil para saber qué se aprendió desde la última sesión.",
|
|
756
|
+
inputSchema: {
|
|
757
|
+
since: z.string().describe("Desde cuándo mostrar cambios (ej: 24h, 7d, 2026-07-10)"),
|
|
758
|
+
type: z.enum(["all", "created", "updated"]).optional().default("all").describe("Filtrar por tipo de cambio"),
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
async ({ since, type }) => {
|
|
762
|
+
const sinceDate = parseRelativeDate(since)
|
|
763
|
+
const today = new Date().toISOString().split("T")[0]
|
|
764
|
+
const data = readMemory()
|
|
765
|
+
const lines = data.split("\n").filter((l) => l.startsWith(" ") && l.includes("|") && !l.startsWith(" summaries:"))
|
|
766
|
+
|
|
767
|
+
const results = lines
|
|
768
|
+
.map((line) => {
|
|
769
|
+
const trimmed = line.trim()
|
|
770
|
+
const parts = trimmed.split("|")
|
|
771
|
+
if (parts.length < 7) return null
|
|
772
|
+
const [id, cat, key, content, file, tags, date] = parts
|
|
773
|
+
if (date < sinceDate) return null
|
|
774
|
+
// For "updated" we check if date is recent but key existed before
|
|
775
|
+
// For simplicity: same date as today = created today, otherwise "updated" if date >= sinceDate
|
|
776
|
+
const changeType = date === today ? "created" : "updated"
|
|
777
|
+
if (type !== "all" && changeType !== type) return null
|
|
778
|
+
return { id, cat, key, content, file, tags, date, changeType }
|
|
779
|
+
})
|
|
780
|
+
.filter(Boolean)
|
|
781
|
+
|
|
782
|
+
if (results.length === 0) {
|
|
783
|
+
return { content: [{ type: "text" as const, text: `No hay cambios desde ${sinceDate}` }] }
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const created = results.filter((r) => r!.changeType === "created")
|
|
787
|
+
const updated = results.filter((r) => r!.changeType === "updated")
|
|
788
|
+
|
|
789
|
+
const sections: string[] = [`📋 Cambios desde ${sinceDate}:`, ""]
|
|
790
|
+
|
|
791
|
+
if (created.length > 0 && (type === "all" || type === "created")) {
|
|
792
|
+
sections.push(`➕ Nuevas (${created.length}):`)
|
|
793
|
+
for (const r of created) {
|
|
794
|
+
sections.push(` [${r!.cat}] ${r!.key} (${r!.id})\n ${r!.content}`)
|
|
795
|
+
}
|
|
796
|
+
sections.push("")
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (updated.length > 0 && (type === "all" || type === "updated")) {
|
|
800
|
+
sections.push(`✏️ Actualizadas (${updated.length}):`)
|
|
801
|
+
for (const r of updated) {
|
|
802
|
+
sections.push(` [${r!.cat}] ${r!.key} (${r!.id}) — ${r!.date}`)
|
|
803
|
+
}
|
|
804
|
+
sections.push("")
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return { content: [{ type: "text" as const, text: sections.join("\n") }] }
|
|
808
|
+
}
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Register the memory_suggest tool.
|
|
813
|
+
* Suggest related entries for a given text context.
|
|
814
|
+
*/
|
|
815
|
+
server.registerTool(
|
|
816
|
+
"memory_suggest",
|
|
817
|
+
{
|
|
818
|
+
title: "Suggest Related Memories",
|
|
819
|
+
description: "Sugiere entradas de memoria relacionadas con un contexto dado. Útil para obtener contexto antes de una tarea.",
|
|
820
|
+
inputSchema: {
|
|
821
|
+
context: z.string().describe("Texto o contexto para buscar sugerencias"),
|
|
822
|
+
limit: z.number().optional().default(5).describe("Máximo de sugerencias"),
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
async ({ context, limit }) => {
|
|
826
|
+
const related = findRelatedEntries(context, "", limit)
|
|
827
|
+
|
|
828
|
+
if (related.length === 0) {
|
|
829
|
+
return { content: [{ type: "text" as const, text: `No se encontraron entradas relacionadas con "${context}"` }] }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const formatted = related
|
|
833
|
+
.map((r) => `[${r.cat}] ${r.key} (${r.id})\n ${r.content}\n File: ${r.file} | Tags: ${r.tags} | Date: ${r.date}`)
|
|
834
|
+
.join("\n\n")
|
|
835
|
+
|
|
836
|
+
return { content: [{ type: "text" as const, text: `🔍 Sugerencias para "${context}":\n\n${formatted}` }] }
|
|
837
|
+
}
|
|
838
|
+
)
|
|
839
|
+
|
|
524
840
|
/**
|
|
525
841
|
* Register the memory_summary tool.
|
|
526
842
|
* Get or set file summaries to save tokens when reading large files.
|
package/uninstall.sh
CHANGED
|
@@ -24,7 +24,7 @@ remove_from_config ".opencode/opencode.json"
|
|
|
24
24
|
# VS Code
|
|
25
25
|
remove_from_config ".vscode/mcp.json"
|
|
26
26
|
|
|
27
|
-
# Claude
|
|
27
|
+
# Claude Code
|
|
28
28
|
remove_from_config "${HOME}/.claude/settings.json"
|
|
29
29
|
remove_from_config ".claude/settings.json"
|
|
30
30
|
|
|
@@ -40,7 +40,28 @@ remove_from_config ".cline/mcp.json"
|
|
|
40
40
|
# Continue
|
|
41
41
|
remove_from_config ".continue/config.json"
|
|
42
42
|
|
|
43
|
-
#
|
|
43
|
+
# Codex CLI
|
|
44
|
+
remove_from_config ".codex/config.toml"
|
|
45
|
+
|
|
46
|
+
# Gemini CLI
|
|
47
|
+
remove_from_config ".gemini/settings.json"
|
|
48
|
+
|
|
49
|
+
# Zed
|
|
50
|
+
remove_from_config "${HOME}/.config/zed/settings.json"
|
|
51
|
+
|
|
52
|
+
# Antigravity
|
|
53
|
+
remove_from_config ".gemini/config/mcp_config.json"
|
|
54
|
+
|
|
55
|
+
# KiloCode
|
|
56
|
+
remove_from_config "${HOME}/.kilocode/mcp_settings.json"
|
|
57
|
+
|
|
58
|
+
# OpenClaw
|
|
59
|
+
remove_from_config ".openclaw.json"
|
|
60
|
+
|
|
61
|
+
# Kiro
|
|
62
|
+
remove_from_config ".kiro/settings/mcp.json"
|
|
63
|
+
|
|
64
|
+
# Remove custom tools (legacy)
|
|
44
65
|
if [ -d ".opencode/tools" ]; then
|
|
45
66
|
if [ -f ".opencode/tools/memory.ts" ]; then
|
|
46
67
|
echo "Removing custom tools..."
|
|
@@ -49,13 +70,20 @@ if [ -d ".opencode/tools" ]; then
|
|
|
49
70
|
fi
|
|
50
71
|
fi
|
|
51
72
|
|
|
73
|
+
# Remove hook scripts
|
|
74
|
+
if [ -d ".toon-memory/hooks" ]; then
|
|
75
|
+
echo "Removing hooks..."
|
|
76
|
+
rm -rf ".toon-memory/hooks"
|
|
77
|
+
echo " ✅ Removed .toon-memory/hooks/"
|
|
78
|
+
fi
|
|
79
|
+
|
|
52
80
|
# Remove memory file (ask user)
|
|
53
|
-
if [ -f ".
|
|
81
|
+
if [ -f ".toon-memory/memory/data.toon" ]; then
|
|
54
82
|
echo ""
|
|
55
|
-
read -p "Remove memory file (.
|
|
83
|
+
read -p "Remove memory file (.toon-memory/memory/)? [y/N] " -n 1 -r
|
|
56
84
|
echo
|
|
57
85
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
58
|
-
rm -rf ".
|
|
86
|
+
rm -rf ".toon-memory/memory"
|
|
59
87
|
echo " ✅ Removed memory directory"
|
|
60
88
|
fi
|
|
61
89
|
fi
|