myagentmemory 0.4.8 → 0.4.10

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/dist/core.js CHANGED
@@ -15,12 +15,14 @@ let MEMORY_DIR = DEFAULT_MEMORY_DIR;
15
15
  let MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
16
16
  let SCRATCHPAD_FILE = path.join(MEMORY_DIR, "SCRATCHPAD.md");
17
17
  let DAILY_DIR = path.join(MEMORY_DIR, "daily");
18
+ let TOPICS_DIR = path.join(MEMORY_DIR, "topics");
18
19
  /** Override base directory (for testing or platform-specific defaults). */
19
20
  export function _setBaseDir(baseDir) {
20
21
  MEMORY_DIR = baseDir;
21
22
  MEMORY_FILE = path.join(baseDir, "MEMORY.md");
22
23
  SCRATCHPAD_FILE = path.join(baseDir, "SCRATCHPAD.md");
23
24
  DAILY_DIR = path.join(baseDir, "daily");
25
+ TOPICS_DIR = path.join(baseDir, "topics");
24
26
  }
25
27
  /** Reset to default paths. */
26
28
  export function _resetBaseDir() {
@@ -42,12 +44,17 @@ export function getScratchpadFile() {
42
44
  export function getDailyDir() {
43
45
  return DAILY_DIR;
44
46
  }
47
+ /** Get the current topics directory path. */
48
+ export function getTopicsDir() {
49
+ return TOPICS_DIR;
50
+ }
45
51
  // ---------------------------------------------------------------------------
46
52
  // Utilities
47
53
  // ---------------------------------------------------------------------------
48
54
  export function ensureDirs() {
49
55
  fs.mkdirSync(MEMORY_DIR, { recursive: true });
50
56
  fs.mkdirSync(DAILY_DIR, { recursive: true });
57
+ fs.mkdirSync(TOPICS_DIR, { recursive: true });
51
58
  }
52
59
  export function todayStr() {
53
60
  const d = new Date();
@@ -78,6 +85,17 @@ export function readFileSafe(filePath) {
78
85
  export function dailyPath(date) {
79
86
  return path.join(DAILY_DIR, `${date}.md`);
80
87
  }
88
+ export function topicPath(slug) {
89
+ return path.join(TOPICS_DIR, `${slug}.md`);
90
+ }
91
+ export function slugifyTopic(name) {
92
+ return name
93
+ .trim()
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9]+/g, "-")
96
+ .replace(/^-+|-+$/g, "")
97
+ .replace(/--+/g, "-");
98
+ }
81
99
  // ---------------------------------------------------------------------------
82
100
  // Limits + preview helpers
83
101
  // ---------------------------------------------------------------------------
@@ -87,6 +105,8 @@ const CONTEXT_LONG_TERM_MAX_CHARS = 4_000;
87
105
  const CONTEXT_LONG_TERM_MAX_LINES = 150;
88
106
  const CONTEXT_SCRATCHPAD_MAX_CHARS = 2_000;
89
107
  const CONTEXT_SCRATCHPAD_MAX_LINES = 120;
108
+ const CONTEXT_TOPICS_MAX_CHARS = 2_000;
109
+ const CONTEXT_TOPICS_MAX_LINES = 100;
90
110
  const CONTEXT_DAILY_MAX_CHARS = 3_000;
91
111
  const CONTEXT_DAILY_MAX_LINES = 120;
92
112
  const CONTEXT_SEARCH_MAX_CHARS = 2_500;
@@ -225,7 +245,7 @@ export function serializeScratchpad(items) {
225
245
  // ---------------------------------------------------------------------------
226
246
  export function buildMemoryContext(searchResults) {
227
247
  ensureDirs();
228
- // Priority order: scratchpad > today's daily > search results > MEMORY.md > yesterday's daily
248
+ // Priority order: scratchpad > topics > today's daily > search results > MEMORY.md > yesterday's daily
229
249
  const sections = [];
230
250
  const scratchpad = readFileSafe(SCRATCHPAD_FILE);
231
251
  if (scratchpad?.trim()) {
@@ -237,6 +257,9 @@ export function buildMemoryContext(searchResults) {
237
257
  sections.push(section);
238
258
  }
239
259
  }
260
+ const topicsSection = buildTopicsContextSection();
261
+ if (topicsSection)
262
+ sections.push(topicsSection);
240
263
  const today = todayStr();
241
264
  const yesterday = yesterdayStr();
242
265
  const todayContent = readFileSafe(dailyPath(today));
@@ -279,6 +302,40 @@ export function buildMemoryContext(searchResults) {
279
302
  }
280
303
  return context;
281
304
  }
305
+ function buildTopicsContextSection() {
306
+ let topicFiles;
307
+ try {
308
+ topicFiles = fs
309
+ .readdirSync(TOPICS_DIR)
310
+ .filter((f) => f.endsWith(".md"))
311
+ .sort();
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ if (topicFiles.length === 0)
317
+ return null;
318
+ const entries = [];
319
+ for (const file of topicFiles) {
320
+ const slug = file.replace(/\.md$/, "");
321
+ const content = readFileSafe(path.join(TOPICS_DIR, file));
322
+ if (!content?.trim())
323
+ continue;
324
+ const titleMatch = content.match(/^# Topic:\s*(.+)$/m);
325
+ const title = titleMatch?.[1]?.trim() || slug;
326
+ entries.push(...parseTopicEntries(title, slug, content));
327
+ }
328
+ if (entries.length === 0)
329
+ return null;
330
+ const recent = sortRecentFirst(entries).slice(0, 8);
331
+ const lines = recent.map((entry) => {
332
+ const firstLine = entry.content.split("\n")[0].slice(0, 120);
333
+ const suffix = entry.content.split("\n")[0].length > 120 ? "..." : "";
334
+ const datePart = entry.date ? ` → [[${entry.date}]]` : "";
335
+ return `- ${entry.topic}: ${firstLine}${suffix}${datePart}`;
336
+ });
337
+ return formatContextSection("## Topics (recent)", lines.join("\n"), "start", CONTEXT_TOPICS_MAX_LINES, CONTEXT_TOPICS_MAX_CHARS);
338
+ }
282
339
  let execFileFn = execFile;
283
340
  let spawnFn = spawn;
284
341
  let qmdAvailable = false;
@@ -353,7 +410,7 @@ function commandExists(cmd) {
353
410
  return false;
354
411
  const dirs = envPath.split(path.delimiter).filter(Boolean);
355
412
  const isWin = process.platform === "win32";
356
- const rawExts = isWin ? process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM" : "";
413
+ const rawExts = isWin ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM") : "";
357
414
  const exts = isWin ? rawExts.split(";").filter(Boolean) : [""];
358
415
  for (const dir of dirs) {
359
416
  for (const ext of exts) {
@@ -439,6 +496,7 @@ export async function setupQmdCollection() {
439
496
  // Add path contexts (best-effort, ignore errors)
440
497
  const contexts = [
441
498
  ["/daily", "Daily append-only work logs organized by date"],
499
+ ["/topics", "Topic and event notes linked back to daily logs"],
442
500
  ["/", "Curated long-term memory: decisions, preferences, facts, lessons"],
443
501
  ];
444
502
  for (const [ctxPath, desc] of contexts) {
@@ -699,6 +757,43 @@ export function installSkills() {
699
757
  skipped,
700
758
  };
701
759
  }
760
+ export function uninstallSkills() {
761
+ const homeDir = resolveHomeDir();
762
+ if (!homeDir) {
763
+ return {
764
+ ok: false,
765
+ removed: [],
766
+ skipped: [],
767
+ error: "Home directory not found. Set HOME (or USERPROFILE on Windows) and retry.",
768
+ };
769
+ }
770
+ const targets = [
771
+ { label: "Claude Code skill", destDir: path.join(homeDir, ".claude", "skills", "agent-memory") },
772
+ { label: "Codex skill", destDir: path.join(homeDir, ".codex", "skills", "agent-memory") },
773
+ { label: "Cursor skill", destDir: path.join(homeDir, ".cursor", "skills", "agent-memory") },
774
+ { label: "Agent CLI skill", destDir: path.join(homeDir, ".agents", "skills", "agent-memory") },
775
+ ];
776
+ const removed = [];
777
+ const skipped = [];
778
+ for (const target of targets) {
779
+ const skillFile = path.join(target.destDir, "SKILL.md");
780
+ if (fs.existsSync(skillFile)) {
781
+ fs.unlinkSync(skillFile);
782
+ // Clean up empty directory
783
+ try {
784
+ fs.rmdirSync(target.destDir);
785
+ }
786
+ catch {
787
+ // directory not empty or already gone — fine
788
+ }
789
+ removed.push({ label: target.label, path: skillFile });
790
+ }
791
+ else {
792
+ skipped.push({ label: target.label, reason: "not installed" });
793
+ }
794
+ }
795
+ return { ok: true, homeDir, removed, skipped };
796
+ }
702
797
  export function parseQmdStatus(stdout, collectionName) {
703
798
  const result = {
704
799
  totalFiles: null,
@@ -854,7 +949,8 @@ export function runQmdSearch(mode, query, limit) {
854
949
  }
855
950
  export async function memoryWrite(params) {
856
951
  ensureDirs();
857
- const { target, content, mode } = params;
952
+ const target = params.target ?? "daily";
953
+ const { content, mode } = params;
858
954
  const sid = shortSessionId(params.sessionId ?? "cli");
859
955
  const ts = nowTimestamp();
860
956
  if (target === "daily") {
@@ -886,6 +982,49 @@ export async function memoryWrite(params) {
886
982
  },
887
983
  };
888
984
  }
985
+ if (target === "topic") {
986
+ const topic = params.topic?.trim();
987
+ if (!topic) {
988
+ return { text: "Error: 'topic' is required for target 'topic'.", details: {}, isError: true };
989
+ }
990
+ const slug = slugifyTopic(topic);
991
+ if (!slug) {
992
+ return { text: "Error: 'topic' must include at least one letter or number.", details: {}, isError: true };
993
+ }
994
+ const filePath = topicPath(slug);
995
+ const existing = readFileSafe(filePath) ?? "";
996
+ const existingPreview = buildPreview(existing, {
997
+ maxLines: RESPONSE_PREVIEW_MAX_LINES,
998
+ maxChars: RESPONSE_PREVIEW_MAX_CHARS,
999
+ mode: "end",
1000
+ });
1001
+ const existingSnippet = existingPreview.preview
1002
+ ? `\n\n${formatPreviewBlock("Existing topic preview", existing, "end")}`
1003
+ : "\n\nTopic file was empty.";
1004
+ const linkDate = params.date?.trim() || todayStr();
1005
+ const header = `# Topic: ${topic}\n\n<!-- created: ${ts} [${sid}] -->\n`;
1006
+ const separator = existing.trim() ? "\n\n" : "";
1007
+ const base = existing.trim() ? existing : header.trimEnd();
1008
+ const stamped = `<!-- ${ts} [${sid}] -->\n${content.trim()}\nDaily: [[${linkDate}]]`;
1009
+ fs.writeFileSync(filePath, `${base}${separator}${stamped}`, "utf-8");
1010
+ await ensureQmdAvailableForUpdate();
1011
+ scheduleQmdUpdate();
1012
+ return {
1013
+ text: `Appended to topic: ${filePath}${existingSnippet}`,
1014
+ details: {
1015
+ path: filePath,
1016
+ target,
1017
+ mode: "append",
1018
+ sessionId: sid,
1019
+ timestamp: ts,
1020
+ topic,
1021
+ slug,
1022
+ date: linkDate,
1023
+ qmdUpdateMode: getQmdUpdateMode(),
1024
+ existingPreview,
1025
+ },
1026
+ };
1027
+ }
889
1028
  // long_term
890
1029
  const memFile = getMemoryFile();
891
1030
  const existing = readFileSafe(memFile) ?? "";
@@ -1053,7 +1192,7 @@ export async function scratchpadAction(params) {
1053
1192
  }
1054
1193
  export async function memoryRead(params) {
1055
1194
  ensureDirs();
1056
- const { target, date } = params;
1195
+ const { target, date, topic } = params;
1057
1196
  if (target === "list") {
1058
1197
  try {
1059
1198
  const files = fs
@@ -1082,6 +1221,38 @@ export async function memoryRead(params) {
1082
1221
  }
1083
1222
  return { text: content, details: { path: filePath, date: d } };
1084
1223
  }
1224
+ if (target === "topics") {
1225
+ try {
1226
+ const files = fs
1227
+ .readdirSync(getTopicsDir())
1228
+ .filter((f) => f.endsWith(".md"))
1229
+ .sort()
1230
+ .reverse();
1231
+ if (files.length === 0) {
1232
+ return { text: "No topics found.", details: {} };
1233
+ }
1234
+ return {
1235
+ text: `Topics:\n${files.map((f) => `- ${f}`).join("\n")}`,
1236
+ details: { files },
1237
+ };
1238
+ }
1239
+ catch {
1240
+ return { text: "No topics directory.", details: {} };
1241
+ }
1242
+ }
1243
+ if (target === "topic") {
1244
+ const name = topic?.trim();
1245
+ if (!name) {
1246
+ return { text: "Error: 'topic' is required for target 'topic'.", details: {}, isError: true };
1247
+ }
1248
+ const slug = slugifyTopic(name);
1249
+ const filePath = topicPath(slug);
1250
+ const content = readFileSafe(filePath);
1251
+ if (!content) {
1252
+ return { text: `No topic file found for ${name}.`, details: {} };
1253
+ }
1254
+ return { text: content, details: { path: filePath, topic: name, slug } };
1255
+ }
1085
1256
  if (target === "scratchpad") {
1086
1257
  const content = readFileSafe(getScratchpadFile());
1087
1258
  if (!content?.trim()) {
@@ -1191,3 +1362,322 @@ export async function memorySearch(params) {
1191
1362
  };
1192
1363
  }
1193
1364
  }
1365
+ // ---------------------------------------------------------------------------
1366
+ // Distil — extraction helpers
1367
+ // ---------------------------------------------------------------------------
1368
+ /** Extract #tag patterns, deduplicated and lowercased. */
1369
+ export function extractTags(content) {
1370
+ const matches = content.match(/(?:^|\s)#([a-zA-Z][\w-]*)/g);
1371
+ if (!matches)
1372
+ return [];
1373
+ const tags = new Set(matches.map((m) => m.trim().toLowerCase()));
1374
+ return [...tags].sort();
1375
+ }
1376
+ /** Extract [[link]] patterns. */
1377
+ export function extractLinks(content) {
1378
+ const matches = content.match(/\[\[([^\]]+)\]\]/g);
1379
+ if (!matches)
1380
+ return [];
1381
+ const links = new Set(matches.map((m) => m.slice(2, -2).trim()));
1382
+ return [...links].sort();
1383
+ }
1384
+ /** Extract file-path-like patterns (e.g. src/foo.ts), filtering out URLs. */
1385
+ export function extractFilePaths(content) {
1386
+ const matches = content.match(/(?:^|\s)((?:[\w.-]+\/)+[\w.-]+\.\w+)/g);
1387
+ if (!matches)
1388
+ return [];
1389
+ const paths = new Set();
1390
+ for (const m of matches) {
1391
+ const p = m.trim();
1392
+ // Skip URLs
1393
+ if (p.startsWith("http://") || p.startsWith("https://") || p.startsWith("//"))
1394
+ continue;
1395
+ paths.add(p);
1396
+ }
1397
+ return [...paths].sort();
1398
+ }
1399
+ /** Extract backtick-quoted commands. */
1400
+ export function extractCommands(content) {
1401
+ const matches = content.match(/`([^`]+)`/g);
1402
+ if (!matches)
1403
+ return [];
1404
+ const cmds = new Set(matches.map((m) => m.slice(1, -1).trim()).filter((c) => c.length > 2 && c.includes(" ")));
1405
+ return [...cmds];
1406
+ }
1407
+ /** Split a daily file on <!-- timestamp --> markers into individual entries. */
1408
+ export function parseDailyEntries(date, content) {
1409
+ const entries = [];
1410
+ // Split on timestamp markers: <!-- 2026-02-21 14:30:00 [sid] -->
1411
+ const parts = content.split(/(?=<!-- \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[)/);
1412
+ for (const part of parts) {
1413
+ const trimmed = part.trim();
1414
+ if (!trimmed)
1415
+ continue;
1416
+ const metaMatch = trimmed.match(/^<!-- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[([^\]]*)\] -->/);
1417
+ const timestamp = metaMatch?.[1] ?? "";
1418
+ const sessionId = metaMatch?.[2] ?? "";
1419
+ const body = metaMatch ? trimmed.slice(metaMatch[0].length).trim() : trimmed;
1420
+ if (!body)
1421
+ continue;
1422
+ entries.push({
1423
+ date,
1424
+ timestamp,
1425
+ sessionId,
1426
+ content: body,
1427
+ tags: extractTags(body),
1428
+ links: extractLinks(body),
1429
+ filePaths: extractFilePaths(body),
1430
+ commands: extractCommands(body),
1431
+ });
1432
+ }
1433
+ return entries;
1434
+ }
1435
+ function stripDailyLinkLines(content) {
1436
+ const lines = content.split("\n");
1437
+ const filtered = lines.filter((line) => !/^\s*Daily:\s*\[\[\d{4}-\d{2}-\d{2}\]\]\s*$/i.test(line) && !/^\s*\[\[\d{4}-\d{2}-\d{2}\]\]\s*$/i.test(line));
1438
+ const cleaned = filtered.join("\n").trim();
1439
+ return cleaned || content.trim();
1440
+ }
1441
+ /** Split a topic file on <!-- timestamp --> markers into entries. */
1442
+ export function parseTopicEntries(topic, slug, content) {
1443
+ const entries = [];
1444
+ const parts = content.split(/(?=<!-- \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[)/);
1445
+ for (const part of parts) {
1446
+ const trimmed = part.trim();
1447
+ if (!trimmed)
1448
+ continue;
1449
+ const metaMatch = trimmed.match(/^<!-- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[([^\]]*)\] -->/);
1450
+ if (!metaMatch)
1451
+ continue;
1452
+ const timestamp = metaMatch[1] ?? "";
1453
+ const sessionId = metaMatch[2] ?? "";
1454
+ const body = trimmed.slice(metaMatch[0].length).trim();
1455
+ if (!body)
1456
+ continue;
1457
+ const dateMatch = body.match(/\[\[(\d{4}-\d{2}-\d{2})\]\]/);
1458
+ const date = dateMatch?.[1] ?? timestamp.slice(0, 10);
1459
+ const cleaned = stripDailyLinkLines(body);
1460
+ entries.push({
1461
+ topic,
1462
+ slug,
1463
+ date,
1464
+ timestamp,
1465
+ sessionId,
1466
+ content: cleaned,
1467
+ tags: extractTags(cleaned),
1468
+ links: extractLinks(cleaned),
1469
+ filePaths: extractFilePaths(cleaned),
1470
+ commands: extractCommands(cleaned),
1471
+ });
1472
+ }
1473
+ return entries;
1474
+ }
1475
+ function summarizeEntry(entry) {
1476
+ const firstLine = entry.content.split("\n")[0].slice(0, 80);
1477
+ const suffix = entry.content.split("\n")[0].length > 80 ? "..." : "";
1478
+ return `${firstLine}${suffix}`;
1479
+ }
1480
+ /** Sort entries recent-first by date then timestamp. */
1481
+ function sortRecentFirst(entries) {
1482
+ return [...entries].sort((a, b) => {
1483
+ if (a.date !== b.date)
1484
+ return b.date.localeCompare(a.date);
1485
+ return b.timestamp.localeCompare(a.timestamp);
1486
+ });
1487
+ }
1488
+ function entryKey(entry) {
1489
+ const base = `${entry.date}:${entry.timestamp}:${entry.sessionId}`;
1490
+ return "topic" in entry ? `topic:${entry.slug}:${base}` : `daily:${base}`;
1491
+ }
1492
+ export async function distilMemories(params) {
1493
+ ensureDirs();
1494
+ const dryRun = params?.dryRun ?? false;
1495
+ // 1. Read all daily + topic files
1496
+ let dailyFiles;
1497
+ try {
1498
+ dailyFiles = fs
1499
+ .readdirSync(DAILY_DIR)
1500
+ .filter((f) => f.endsWith(".md"))
1501
+ .sort();
1502
+ }
1503
+ catch {
1504
+ dailyFiles = [];
1505
+ }
1506
+ let topicFiles;
1507
+ try {
1508
+ topicFiles = fs
1509
+ .readdirSync(TOPICS_DIR)
1510
+ .filter((f) => f.endsWith(".md"))
1511
+ .sort();
1512
+ }
1513
+ catch {
1514
+ topicFiles = [];
1515
+ }
1516
+ if (dailyFiles.length === 0 && topicFiles.length === 0) {
1517
+ return {
1518
+ ok: true,
1519
+ dryRun,
1520
+ totalDailyFiles: 0,
1521
+ totalTopicFiles: 0,
1522
+ totalEntries: 0,
1523
+ totalTopicEntries: 0,
1524
+ totalTags: 0,
1525
+ tagCounts: {},
1526
+ output: "# Memory Index\n\nNo daily logs or topics to distil.\n",
1527
+ };
1528
+ }
1529
+ // 2. Parse all entries
1530
+ const allEntries = [];
1531
+ for (const file of dailyFiles) {
1532
+ const date = file.replace(/\.md$/, "");
1533
+ const content = readFileSafe(path.join(DAILY_DIR, file));
1534
+ if (!content?.trim())
1535
+ continue;
1536
+ allEntries.push(...parseDailyEntries(date, content));
1537
+ }
1538
+ const topicEntriesByTopic = new Map();
1539
+ let totalTopicEntries = 0;
1540
+ for (const file of topicFiles) {
1541
+ const slug = file.replace(/\.md$/, "");
1542
+ const content = readFileSafe(path.join(TOPICS_DIR, file));
1543
+ if (!content?.trim())
1544
+ continue;
1545
+ const titleMatch = content.match(/^# Topic:\s*(.+)$/m);
1546
+ const title = titleMatch?.[1]?.trim() || slug;
1547
+ const entries = parseTopicEntries(title, slug, content);
1548
+ if (entries.length === 0)
1549
+ continue;
1550
+ totalTopicEntries += entries.length;
1551
+ allEntries.push(...entries);
1552
+ topicEntriesByTopic.set(title, entries);
1553
+ }
1554
+ if (allEntries.length === 0) {
1555
+ return {
1556
+ ok: true,
1557
+ dryRun,
1558
+ totalDailyFiles: dailyFiles.length,
1559
+ totalTopicFiles: topicFiles.length,
1560
+ totalEntries: 0,
1561
+ totalTopicEntries: 0,
1562
+ totalTags: 0,
1563
+ tagCounts: {},
1564
+ output: "# Memory Index\n\nNo entries found in daily logs or topics.\n",
1565
+ };
1566
+ }
1567
+ // 3. Build tag → entries map and tag → dates map
1568
+ const tagEntries = new Map();
1569
+ const tagDates = new Map();
1570
+ const untagged = [];
1571
+ for (const entry of allEntries) {
1572
+ if (entry.tags.length === 0) {
1573
+ untagged.push(entry);
1574
+ }
1575
+ else {
1576
+ for (const tag of entry.tags) {
1577
+ if (!tagEntries.has(tag))
1578
+ tagEntries.set(tag, []);
1579
+ tagEntries.get(tag).push(entry);
1580
+ if (!tagDates.has(tag))
1581
+ tagDates.set(tag, new Set());
1582
+ tagDates.get(tag).add(entry.date);
1583
+ }
1584
+ }
1585
+ }
1586
+ // Sort tags by entry count (most used first), take top 15 for sections
1587
+ const sortedTags = [...tagEntries.entries()].sort((a, b) => b[1].length - a[1].length);
1588
+ const sectionTags = sortedTags.slice(0, 15);
1589
+ const indexTags = sortedTags.slice(0, 20);
1590
+ // 4. Preserve existing ## Pinned section from MEMORY.md
1591
+ let pinnedSection = "";
1592
+ const existingMemory = readFileSafe(MEMORY_FILE);
1593
+ if (existingMemory) {
1594
+ const pinnedMatch = existingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
1595
+ if (pinnedMatch) {
1596
+ pinnedSection = pinnedMatch[1].trim();
1597
+ }
1598
+ }
1599
+ // 5. Generate output
1600
+ const lines = [];
1601
+ const ts = nowTimestamp();
1602
+ lines.push("# Memory Index");
1603
+ lines.push(`<!-- last distilled: ${ts} -->`);
1604
+ lines.push("");
1605
+ // Pinned section
1606
+ if (pinnedSection) {
1607
+ lines.push("## Pinned");
1608
+ lines.push(pinnedSection);
1609
+ lines.push("");
1610
+ }
1611
+ // Topics section — list topics with latest entry summary
1612
+ if (topicEntriesByTopic.size > 0) {
1613
+ lines.push("## Topics");
1614
+ const sortedTopics = [...topicEntriesByTopic.entries()].sort((a, b) => b[1].length - a[1].length);
1615
+ for (const [topic, entries] of sortedTopics) {
1616
+ const recent = sortRecentFirst(entries)[0];
1617
+ const summary = summarizeEntry(recent);
1618
+ const dateLink = recent.date ? `[[${recent.date}]]` : "";
1619
+ const filePath = `topics/${entries[0].slug}.md`;
1620
+ const parts = [
1621
+ `${topic} — ${summary}`,
1622
+ dateLink ? `→ ${dateLink}` : "",
1623
+ `(${entries.length} entries)`,
1624
+ `(${filePath})`,
1625
+ ].filter(Boolean);
1626
+ lines.push(`- ${parts.join(" ")}`);
1627
+ }
1628
+ lines.push("");
1629
+ }
1630
+ // Tag-based sections — top tags become section headers, each capped at 3 entries
1631
+ const tagCounts = {};
1632
+ const shownEntryIds = new Set();
1633
+ for (const [tag, entries] of sectionTags) {
1634
+ tagCounts[tag] = entries.length;
1635
+ lines.push(`## ${tag}`);
1636
+ const recent = sortRecentFirst(entries).slice(0, 3);
1637
+ for (const entry of recent) {
1638
+ const entryId = entryKey(entry);
1639
+ shownEntryIds.add(entryId);
1640
+ lines.push(`- ${summarizeEntry(entry)} → [[${entry.date}]]`);
1641
+ }
1642
+ lines.push("");
1643
+ }
1644
+ // Recent untagged entries (up to 5, not already shown)
1645
+ const unseenUntagged = sortRecentFirst(untagged)
1646
+ .filter((e) => !shownEntryIds.has(entryKey(e)))
1647
+ .slice(0, 5);
1648
+ if (unseenUntagged.length > 0) {
1649
+ lines.push("## Recent");
1650
+ for (const entry of unseenUntagged) {
1651
+ lines.push(`- ${summarizeEntry(entry)} → [[${entry.date}]]`);
1652
+ }
1653
+ lines.push("");
1654
+ }
1655
+ // Tag index — all tags with their dates
1656
+ if (indexTags.length > 0) {
1657
+ lines.push("## Tags");
1658
+ for (const [tag] of indexTags) {
1659
+ const dates = tagDates.get(tag);
1660
+ const recentDates = [...dates].sort().reverse().slice(0, 5);
1661
+ lines.push(`${tag} → ${recentDates.join(", ")}`);
1662
+ }
1663
+ lines.push("");
1664
+ }
1665
+ const output = lines.join("\n");
1666
+ // 6. Write if not dry-run
1667
+ if (!dryRun) {
1668
+ fs.writeFileSync(MEMORY_FILE, output, "utf-8");
1669
+ await ensureQmdAvailableForUpdate();
1670
+ scheduleQmdUpdate();
1671
+ }
1672
+ return {
1673
+ ok: true,
1674
+ dryRun,
1675
+ totalDailyFiles: dailyFiles.length,
1676
+ totalTopicFiles: topicFiles.length,
1677
+ totalEntries: allEntries.length,
1678
+ totalTopicEntries,
1679
+ totalTags: indexTags.length,
1680
+ tagCounts,
1681
+ output,
1682
+ };
1683
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagentmemory",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "Persistent memory for coding agents (Claude Code, Codex, Cursor, Agent) with qmd-powered semantic search across daily logs, long-term memory, and scratchpad",
5
5
  "main": "./dist/core.js",
6
6
  "types": "./dist/core.d.ts",