myagentmemory 0.4.7 → 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/README.md +32 -7
- package/dist/agent-memory +0 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +151 -12
- package/dist/core.d.ts +77 -2
- package/dist/core.js +502 -4
- package/package.json +1 -1
- package/scripts/install-skills.ps1 +5 -2
- package/scripts/install-skills.sh +54 -11
- package/skills/agent/SKILL.md +39 -14
- package/skills/claude-code/SKILL.md +39 -14
- package/skills/codex/SKILL.md +39 -14
- package/skills/cursor/SKILL.md +39 -14
- package/src/cli.ts +167 -14
- package/src/core.ts +605 -6
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) {
|
|
@@ -598,6 +656,7 @@ export function installSkills() {
|
|
|
598
656
|
detected: [],
|
|
599
657
|
installed: [],
|
|
600
658
|
skipped: [],
|
|
659
|
+
checked: [],
|
|
601
660
|
error: "Home directory not found. Set HOME (or USERPROFILE on Windows) and retry.",
|
|
602
661
|
};
|
|
603
662
|
}
|
|
@@ -609,6 +668,7 @@ export function installSkills() {
|
|
|
609
668
|
detected: [],
|
|
610
669
|
installed: [],
|
|
611
670
|
skipped: [],
|
|
671
|
+
checked: [],
|
|
612
672
|
error: "Could not locate the skills directory. Ensure the package includes skills/ (reinstall if needed).",
|
|
613
673
|
};
|
|
614
674
|
}
|
|
@@ -621,6 +681,7 @@ export function installSkills() {
|
|
|
621
681
|
detected: [],
|
|
622
682
|
installed: [],
|
|
623
683
|
skipped: [],
|
|
684
|
+
checked: [],
|
|
624
685
|
error: `Skills directory not found: ${skillsDir}`,
|
|
625
686
|
};
|
|
626
687
|
}
|
|
@@ -660,9 +721,11 @@ export function installSkills() {
|
|
|
660
721
|
const detected = [];
|
|
661
722
|
const installed = [];
|
|
662
723
|
const skipped = [];
|
|
724
|
+
const checked = [];
|
|
663
725
|
for (const target of targets) {
|
|
664
726
|
if (!fs.existsSync(target.homeMarker)) {
|
|
665
727
|
skipped.push({ label: target.label, reason: `${target.homeMarker} not found` });
|
|
728
|
+
checked.push({ label: target.label, status: "skipped", reason: `${target.homeMarker} not found` });
|
|
666
729
|
continue;
|
|
667
730
|
}
|
|
668
731
|
const detectedByFile = (target.detectFiles ?? []).some((file) => fs.existsSync(file));
|
|
@@ -670,9 +733,11 @@ export function installSkills() {
|
|
|
670
733
|
const requiresDetection = (target.detectFiles?.length ?? 0) > 0 || !!target.detectCommand;
|
|
671
734
|
if (requiresDetection && !detectedByFile && !detectedByCommand) {
|
|
672
735
|
skipped.push({ label: target.label, reason: "not detected" });
|
|
736
|
+
checked.push({ label: target.label, status: "skipped", reason: "not detected" });
|
|
673
737
|
continue;
|
|
674
738
|
}
|
|
675
739
|
detected.push({ label: target.label, homeMarker: target.homeMarker });
|
|
740
|
+
checked.push({ label: target.label, status: "detected" });
|
|
676
741
|
const skillFile = path.join(target.srcDir, "SKILL.md");
|
|
677
742
|
if (!fs.existsSync(skillFile)) {
|
|
678
743
|
skipped.push({ label: target.label, reason: `${skillFile} not found` });
|
|
@@ -686,11 +751,49 @@ export function installSkills() {
|
|
|
686
751
|
ok: true,
|
|
687
752
|
projectDir,
|
|
688
753
|
homeDir,
|
|
754
|
+
checked,
|
|
689
755
|
detected,
|
|
690
756
|
installed,
|
|
691
757
|
skipped,
|
|
692
758
|
};
|
|
693
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
|
+
}
|
|
694
797
|
export function parseQmdStatus(stdout, collectionName) {
|
|
695
798
|
const result = {
|
|
696
799
|
totalFiles: null,
|
|
@@ -846,7 +949,8 @@ export function runQmdSearch(mode, query, limit) {
|
|
|
846
949
|
}
|
|
847
950
|
export async function memoryWrite(params) {
|
|
848
951
|
ensureDirs();
|
|
849
|
-
const
|
|
952
|
+
const target = params.target ?? "daily";
|
|
953
|
+
const { content, mode } = params;
|
|
850
954
|
const sid = shortSessionId(params.sessionId ?? "cli");
|
|
851
955
|
const ts = nowTimestamp();
|
|
852
956
|
if (target === "daily") {
|
|
@@ -878,6 +982,49 @@ export async function memoryWrite(params) {
|
|
|
878
982
|
},
|
|
879
983
|
};
|
|
880
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
|
+
}
|
|
881
1028
|
// long_term
|
|
882
1029
|
const memFile = getMemoryFile();
|
|
883
1030
|
const existing = readFileSafe(memFile) ?? "";
|
|
@@ -1045,7 +1192,7 @@ export async function scratchpadAction(params) {
|
|
|
1045
1192
|
}
|
|
1046
1193
|
export async function memoryRead(params) {
|
|
1047
1194
|
ensureDirs();
|
|
1048
|
-
const { target, date } = params;
|
|
1195
|
+
const { target, date, topic } = params;
|
|
1049
1196
|
if (target === "list") {
|
|
1050
1197
|
try {
|
|
1051
1198
|
const files = fs
|
|
@@ -1074,6 +1221,38 @@ export async function memoryRead(params) {
|
|
|
1074
1221
|
}
|
|
1075
1222
|
return { text: content, details: { path: filePath, date: d } };
|
|
1076
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
|
+
}
|
|
1077
1256
|
if (target === "scratchpad") {
|
|
1078
1257
|
const content = readFileSafe(getScratchpadFile());
|
|
1079
1258
|
if (!content?.trim()) {
|
|
@@ -1183,3 +1362,322 @@ export async function memorySearch(params) {
|
|
|
1183
1362
|
};
|
|
1184
1363
|
}
|
|
1185
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.
|
|
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",
|
|
@@ -12,19 +12,22 @@ function Install-Skill {
|
|
|
12
12
|
[ScriptBlock]$Detect
|
|
13
13
|
)
|
|
14
14
|
|
|
15
|
+
Write-Host "Detecting $Label..."
|
|
15
16
|
if (-not (Test-Path $HomeMarker)) {
|
|
16
|
-
Write-Host "
|
|
17
|
+
Write-Host "Not found ($HomeMarker not found)"
|
|
17
18
|
return
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
if ($Detect -and -not (& $Detect)) {
|
|
21
|
-
Write-Host "
|
|
22
|
+
Write-Host "Not found (not detected)"
|
|
22
23
|
return
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
Write-Host "Found"
|
|
25
27
|
$SkillSrc = Join-Path $SrcDir "SKILL.md"
|
|
26
28
|
if (Test-Path $SkillSrc) {
|
|
27
29
|
New-Item -ItemType Directory -Force -Path $DestDir | Out-Null
|
|
30
|
+
Write-Host "Installing to $DestDir\\SKILL.md"
|
|
28
31
|
Copy-Item -Force $SkillSrc (Join-Path $DestDir "SKILL.md")
|
|
29
32
|
Write-Host "Installed $Label: $DestDir\\SKILL.md"
|
|
30
33
|
} else {
|