myagentmemory 0.4.12 → 0.4.13
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 +83 -38
- package/dist/cli.js +60 -67
- package/dist/core.d.ts +21 -1
- package/dist/core.js +282 -50
- package/package.json +29 -11
- package/scripts/install-skills.sh +4 -1
- package/scripts/postinstall.cjs +23 -4
- package/src/cli.ts +64 -78
- package/src/core.ts +297 -50
- package/dist/agent-memory +0 -0
package/dist/core.js
CHANGED
|
@@ -115,6 +115,100 @@ const CONTEXT_MAX_CHARS = 16_000;
|
|
|
115
115
|
function normalizeContent(content) {
|
|
116
116
|
return content.trim();
|
|
117
117
|
}
|
|
118
|
+
const SECRET_PATTERNS = [
|
|
119
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
120
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
121
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
122
|
+
/\b(?:api[_-]?key|access[_-]?token|secret)\s*[:=]\s*["']?[A-Za-z0-9_./+=-]{16,}["']?/gi,
|
|
123
|
+
];
|
|
124
|
+
/** Redact common credential shapes before content reaches disk or agent context. */
|
|
125
|
+
export function redactSecrets(content) {
|
|
126
|
+
let redactedContent = content;
|
|
127
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
128
|
+
redactedContent = redactedContent.replace(pattern, "[REDACTED_SECRET]");
|
|
129
|
+
}
|
|
130
|
+
return { content: redactedContent, redacted: redactedContent !== content };
|
|
131
|
+
}
|
|
132
|
+
function isInactiveMemoryEntry(entry, now) {
|
|
133
|
+
const metadataHeader = entry.split(/\n\s*\n/, 1)[0];
|
|
134
|
+
if (/^\s*(?:[-*]\s*)?Trust:\s*untrusted\s*\.?\s*$/im.test(metadataHeader))
|
|
135
|
+
return true;
|
|
136
|
+
if (/^\s*(?:[-*]\s*)?Status:\s*(?:expired|superseded|revoked|retired)\s*\.?\s*$/im.test(metadataHeader)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
const validUntil = metadataHeader.match(/^\s*(?:[-*]\s*)?Valid until:?\s+(\d{4}-\d{2}-\d{2})\s*\.?\s*$/im)?.[1];
|
|
140
|
+
if (validUntil && validUntil < now.toISOString().slice(0, 10))
|
|
141
|
+
return true;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
function splitLogicalMemoryEntries(content) {
|
|
145
|
+
const normalized = content.replace(/^\uFEFF/, "");
|
|
146
|
+
const marker = /^<!--\s*(?:last updated:\s*)?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[[^\]]+\]\s*-->$/gm;
|
|
147
|
+
const starts = [...normalized.matchAll(marker)].map((match) => match.index ?? 0);
|
|
148
|
+
if (starts.length === 0) {
|
|
149
|
+
return { entries: normalized.split(/\n\s*\n/), timestampDelimited: false };
|
|
150
|
+
}
|
|
151
|
+
const entries = [];
|
|
152
|
+
const preamble = normalized.slice(0, starts[0]).trim();
|
|
153
|
+
if (preamble)
|
|
154
|
+
entries.push(preamble);
|
|
155
|
+
for (let i = 0; i < starts.length; i++) {
|
|
156
|
+
entries.push(normalized.slice(starts[i], starts[i + 1] ?? normalized.length).trim());
|
|
157
|
+
}
|
|
158
|
+
return { entries, timestampDelimited: true };
|
|
159
|
+
}
|
|
160
|
+
function isUnmarkedInactiveHeader(entry, now) {
|
|
161
|
+
if (!isInactiveMemoryEntry(entry, now))
|
|
162
|
+
return false;
|
|
163
|
+
return entry.split("\n").every((line) => {
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
return (!trimmed ||
|
|
166
|
+
/^#{1,6}\s+/.test(trimmed) ||
|
|
167
|
+
/^<!--.*-->$/.test(trimmed) ||
|
|
168
|
+
/^(?:[-*]\s*)?(?:Trust:|Status:|Valid until:?\s|Source:)/i.test(trimmed));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** Apply trust, lifecycle, and secret policy to complete logical write entries. */
|
|
172
|
+
export function filterMemoryForContext(content, now = new Date()) {
|
|
173
|
+
const { entries, timestampDelimited } = splitLogicalMemoryEntries(content);
|
|
174
|
+
const activeEntries = [];
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
const inactive = isInactiveMemoryEntry(entry, now);
|
|
177
|
+
if (!timestampDelimited && isUnmarkedInactiveHeader(entry, now)) {
|
|
178
|
+
// Without write markers there is no reliable boundary after a metadata-only
|
|
179
|
+
// header. Fail closed instead of treating the following body paragraphs as
|
|
180
|
+
// independent trusted entries.
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (!inactive)
|
|
184
|
+
activeEntries.push(entry);
|
|
185
|
+
}
|
|
186
|
+
return redactSecrets(activeEntries.join("\n\n")).content.trim();
|
|
187
|
+
}
|
|
188
|
+
function sanitizeSourceUri(sourceUri) {
|
|
189
|
+
if (!sourceUri?.trim())
|
|
190
|
+
return undefined;
|
|
191
|
+
const singleLine = [...sourceUri]
|
|
192
|
+
.map((char) => {
|
|
193
|
+
const code = char.charCodeAt(0);
|
|
194
|
+
return code <= 31 || code === 127 ? " " : char;
|
|
195
|
+
})
|
|
196
|
+
.join("")
|
|
197
|
+
.trim()
|
|
198
|
+
.slice(0, 2_048);
|
|
199
|
+
return redactSecrets(singleLine).content;
|
|
200
|
+
}
|
|
201
|
+
function escapeEntryMarkers(content) {
|
|
202
|
+
return content.replace(/^<!--\s*(?:last updated:\s*)?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[[^\]]+\]\s*-->$/gm, (line) => line.replace("<!--", "<!--"));
|
|
203
|
+
}
|
|
204
|
+
function formatStoredEntry(content, metadata, sourceUri) {
|
|
205
|
+
const safeContent = redactSecrets(escapeEntryMarkers(content));
|
|
206
|
+
const source = sanitizeSourceUri(sourceUri);
|
|
207
|
+
return {
|
|
208
|
+
entry: `${metadata}\n${safeContent.content}${source ? `\nSource: ${source}` : ""}`,
|
|
209
|
+
redacted: safeContent.redacted || (!!sourceUri && source !== sourceUri.trim()),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
118
212
|
export function truncateLines(lines, maxLines, mode) {
|
|
119
213
|
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
120
214
|
return { lines, truncated: false };
|
|
@@ -251,7 +345,7 @@ export function buildMemoryContext(searchResults) {
|
|
|
251
345
|
if (scratchpad?.trim()) {
|
|
252
346
|
const openItems = parseScratchpad(scratchpad).filter((i) => !i.done);
|
|
253
347
|
if (openItems.length > 0) {
|
|
254
|
-
const serialized = serializeScratchpad(openItems);
|
|
348
|
+
const serialized = filterMemoryForContext(serializeScratchpad(openItems));
|
|
255
349
|
const section = formatContextSection("## SCRATCHPAD.md (working context)", serialized, "start", CONTEXT_SCRATCHPAD_MAX_LINES, CONTEXT_SCRATCHPAD_MAX_CHARS);
|
|
256
350
|
if (section)
|
|
257
351
|
sections.push(section);
|
|
@@ -263,25 +357,29 @@ export function buildMemoryContext(searchResults) {
|
|
|
263
357
|
const today = todayStr();
|
|
264
358
|
const yesterday = yesterdayStr();
|
|
265
359
|
const todayContent = readFileSafe(dailyPath(today));
|
|
266
|
-
|
|
267
|
-
|
|
360
|
+
const safeTodayContent = todayContent ? filterMemoryForContext(todayContent) : "";
|
|
361
|
+
if (safeTodayContent) {
|
|
362
|
+
const section = formatContextSection(`## Daily log: ${today} (today)`, safeTodayContent, "middle", CONTEXT_DAILY_MAX_LINES, CONTEXT_DAILY_MAX_CHARS);
|
|
268
363
|
if (section)
|
|
269
364
|
sections.push(section);
|
|
270
365
|
}
|
|
271
|
-
|
|
272
|
-
|
|
366
|
+
const safeSearchResults = searchResults ? filterMemoryForContext(searchResults) : "";
|
|
367
|
+
if (safeSearchResults) {
|
|
368
|
+
const section = formatContextSection("## Relevant memories (auto-retrieved)", safeSearchResults, "start", CONTEXT_SEARCH_MAX_LINES, CONTEXT_SEARCH_MAX_CHARS);
|
|
273
369
|
if (section)
|
|
274
370
|
sections.push(section);
|
|
275
371
|
}
|
|
276
372
|
const longTerm = readFileSafe(MEMORY_FILE);
|
|
277
|
-
|
|
278
|
-
|
|
373
|
+
const safeLongTerm = longTerm ? filterMemoryForContext(longTerm) : "";
|
|
374
|
+
if (safeLongTerm) {
|
|
375
|
+
const section = formatContextSection("## MEMORY.md (long-term)", safeLongTerm, "middle", CONTEXT_LONG_TERM_MAX_LINES, CONTEXT_LONG_TERM_MAX_CHARS);
|
|
279
376
|
if (section)
|
|
280
377
|
sections.push(section);
|
|
281
378
|
}
|
|
282
379
|
const yesterdayContent = readFileSafe(dailyPath(yesterday));
|
|
283
|
-
|
|
284
|
-
|
|
380
|
+
const safeYesterdayContent = yesterdayContent ? filterMemoryForContext(yesterdayContent) : "";
|
|
381
|
+
if (safeYesterdayContent) {
|
|
382
|
+
const section = formatContextSection(`## Daily log: ${yesterday} (yesterday)`, safeYesterdayContent, "end", CONTEXT_DAILY_MAX_LINES, CONTEXT_DAILY_MAX_CHARS);
|
|
285
383
|
if (section)
|
|
286
384
|
sections.push(section);
|
|
287
385
|
}
|
|
@@ -290,15 +388,8 @@ export function buildMemoryContext(searchResults) {
|
|
|
290
388
|
}
|
|
291
389
|
const context = `# Memory\n\n${sections.join("\n\n---\n\n")}`;
|
|
292
390
|
if (context.length > CONTEXT_MAX_CHARS) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
maxChars: CONTEXT_MAX_CHARS,
|
|
296
|
-
mode: "start",
|
|
297
|
-
});
|
|
298
|
-
const note = result.truncated
|
|
299
|
-
? `\n\n[truncated overall context: showing ${result.previewChars}/${result.totalChars} chars]`
|
|
300
|
-
: "";
|
|
301
|
-
return `${result.preview}${note}`;
|
|
391
|
+
const note = "\n\n[truncated overall context to 16000 chars]";
|
|
392
|
+
return context.slice(0, CONTEXT_MAX_CHARS - note.length).trimEnd() + note;
|
|
302
393
|
}
|
|
303
394
|
return context;
|
|
304
395
|
}
|
|
@@ -319,11 +410,12 @@ function buildTopicsContextSection() {
|
|
|
319
410
|
for (const file of topicFiles) {
|
|
320
411
|
const slug = file.replace(/\.md$/, "");
|
|
321
412
|
const content = readFileSafe(path.join(TOPICS_DIR, file));
|
|
322
|
-
|
|
413
|
+
const safeContent = content ? filterMemoryForContext(content) : "";
|
|
414
|
+
if (!safeContent)
|
|
323
415
|
continue;
|
|
324
|
-
const titleMatch =
|
|
416
|
+
const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
|
|
325
417
|
const title = titleMatch?.[1]?.trim() || slug;
|
|
326
|
-
entries.push(...parseTopicEntries(title, slug,
|
|
418
|
+
entries.push(...parseTopicEntries(title, slug, safeContent));
|
|
327
419
|
}
|
|
328
420
|
if (entries.length === 0)
|
|
329
421
|
return null;
|
|
@@ -711,6 +803,12 @@ export function installSkills() {
|
|
|
711
803
|
destDir: path.join(homeDir, ".cursor", "skills", "agent-memory"),
|
|
712
804
|
homeMarker: path.join(homeDir, ".cursor"),
|
|
713
805
|
},
|
|
806
|
+
{
|
|
807
|
+
label: "Agent CLI skill",
|
|
808
|
+
srcDir: path.join(skillsDir, "agent"),
|
|
809
|
+
destDir: path.join(homeDir, ".agents", "skills", "agent-memory"),
|
|
810
|
+
homeMarker: path.join(homeDir, ".agents"),
|
|
811
|
+
},
|
|
714
812
|
];
|
|
715
813
|
const detected = [];
|
|
716
814
|
const installed = [];
|
|
@@ -765,6 +863,7 @@ export function uninstallSkills() {
|
|
|
765
863
|
{ label: "Claude Code skill", destDir: path.join(homeDir, ".claude", "skills", "agent-memory") },
|
|
766
864
|
{ label: "Codex skill", destDir: path.join(homeDir, ".codex", "skills", "agent-memory") },
|
|
767
865
|
{ label: "Cursor skill", destDir: path.join(homeDir, ".cursor", "skills", "agent-memory") },
|
|
866
|
+
{ label: "Agent CLI skill", destDir: path.join(homeDir, ".agents", "skills", "agent-memory") },
|
|
768
867
|
];
|
|
769
868
|
const removed = [];
|
|
770
869
|
const skipped = [];
|
|
@@ -844,10 +943,63 @@ export async function getQmdHealth() {
|
|
|
844
943
|
});
|
|
845
944
|
});
|
|
846
945
|
}
|
|
946
|
+
function resolveCaseInsensitivePath(root, relativePath) {
|
|
947
|
+
let current = root;
|
|
948
|
+
for (const segment of relativePath.split(/[/\\]+/).filter(Boolean)) {
|
|
949
|
+
if (segment === "." || segment === "..")
|
|
950
|
+
return null;
|
|
951
|
+
let children;
|
|
952
|
+
try {
|
|
953
|
+
children = fs.readdirSync(current);
|
|
954
|
+
}
|
|
955
|
+
catch {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
const child = children.find((name) => name === segment) ??
|
|
959
|
+
children.find((name) => name.toLowerCase() === segment.toLowerCase());
|
|
960
|
+
if (!child)
|
|
961
|
+
return null;
|
|
962
|
+
current = path.join(current, child);
|
|
963
|
+
}
|
|
964
|
+
return current;
|
|
965
|
+
}
|
|
966
|
+
function resolveQmdSourcePath(filePath) {
|
|
967
|
+
const qmdUri = filePath.match(/^qmd:\/\/([^/]+)\/?(.*)$/i);
|
|
968
|
+
let relativePath = qmdUri ? qmdUri[2] : filePath;
|
|
969
|
+
if (relativePath.toLowerCase().startsWith(`${QMD_COLLECTION_NAME.toLowerCase()}/`)) {
|
|
970
|
+
relativePath = relativePath.slice(QMD_COLLECTION_NAME.length + 1);
|
|
971
|
+
}
|
|
972
|
+
const root = path.resolve(MEMORY_DIR);
|
|
973
|
+
if (path.isAbsolute(relativePath)) {
|
|
974
|
+
const candidate = path.resolve(relativePath);
|
|
975
|
+
if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`))
|
|
976
|
+
return null;
|
|
977
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
978
|
+
}
|
|
979
|
+
return resolveCaseInsensitivePath(root, relativePath.replace(/^\/+/, ""));
|
|
980
|
+
}
|
|
981
|
+
function qmdResultPassesSourcePolicy(filePath, snippet) {
|
|
982
|
+
if (!filePath)
|
|
983
|
+
return false;
|
|
984
|
+
const sourcePath = resolveQmdSourcePath(filePath);
|
|
985
|
+
if (!sourcePath)
|
|
986
|
+
return false;
|
|
987
|
+
const source = readFileSafe(sourcePath);
|
|
988
|
+
if (!source)
|
|
989
|
+
return false;
|
|
990
|
+
const activeSource = filterMemoryForContext(source);
|
|
991
|
+
const snippetLines = snippet
|
|
992
|
+
.split("\n")
|
|
993
|
+
.map((line) => line.trim())
|
|
994
|
+
.filter((line) => line.length >= 8);
|
|
995
|
+
return snippetLines.length > 0 && snippetLines.every((line) => activeSource.includes(line));
|
|
996
|
+
}
|
|
847
997
|
/** Search for memories relevant to the user's prompt. Returns formatted markdown or empty string on error. */
|
|
848
998
|
export async function searchRelevantMemories(prompt) {
|
|
849
999
|
if (!qmdAvailable || !prompt.trim())
|
|
850
1000
|
return "";
|
|
1001
|
+
let timer;
|
|
1002
|
+
const controller = new AbortController();
|
|
851
1003
|
// Sanitize: strip control chars, limit to 200 chars for the search query
|
|
852
1004
|
const sanitized = prompt
|
|
853
1005
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: we intentionally strip control chars.
|
|
@@ -861,19 +1013,26 @@ export async function searchRelevantMemories(prompt) {
|
|
|
861
1013
|
if (!hasCollection)
|
|
862
1014
|
return "";
|
|
863
1015
|
const results = await Promise.race([
|
|
864
|
-
runQmdSearch("keyword", sanitized, 3),
|
|
865
|
-
new Promise((_, reject) =>
|
|
1016
|
+
runQmdSearch("keyword", sanitized, 3, { signal: controller.signal }),
|
|
1017
|
+
new Promise((_, reject) => {
|
|
1018
|
+
timer = setTimeout(() => {
|
|
1019
|
+
controller.abort();
|
|
1020
|
+
reject(new Error("timeout"));
|
|
1021
|
+
}, 3_000);
|
|
1022
|
+
}),
|
|
866
1023
|
]);
|
|
867
1024
|
if (!results || results.results.length === 0)
|
|
868
1025
|
return "";
|
|
869
1026
|
const snippets = results.results
|
|
870
1027
|
.map((r) => {
|
|
871
|
-
const text = getQmdResultText(r);
|
|
872
|
-
if (!text
|
|
1028
|
+
const text = filterMemoryForContext(getQmdResultText(r));
|
|
1029
|
+
if (!text)
|
|
873
1030
|
return null;
|
|
874
1031
|
const filePath = getQmdResultPath(r);
|
|
1032
|
+
if (!qmdResultPassesSourcePolicy(filePath, text))
|
|
1033
|
+
return null;
|
|
875
1034
|
const filePart = filePath ? `_${filePath}_` : "";
|
|
876
|
-
return filePart ? `${filePart}\n${text
|
|
1035
|
+
return filePart ? `${filePart}\n${text}` : text;
|
|
877
1036
|
})
|
|
878
1037
|
.filter(Boolean);
|
|
879
1038
|
if (snippets.length === 0)
|
|
@@ -883,12 +1042,27 @@ export async function searchRelevantMemories(prompt) {
|
|
|
883
1042
|
catch {
|
|
884
1043
|
return "";
|
|
885
1044
|
}
|
|
1045
|
+
finally {
|
|
1046
|
+
clearTimeout(timer);
|
|
1047
|
+
}
|
|
886
1048
|
}
|
|
887
1049
|
export function getQmdResultPath(r) {
|
|
888
1050
|
return r.path ?? r.file;
|
|
889
1051
|
}
|
|
890
1052
|
export function getQmdResultText(r) {
|
|
891
|
-
|
|
1053
|
+
const text = r.content ?? r.chunk ?? r.snippet ?? "";
|
|
1054
|
+
const folderContext = r.context?.trim();
|
|
1055
|
+
let normalized = text.trimStart();
|
|
1056
|
+
if (folderContext) {
|
|
1057
|
+
const prefix = `Folder Context: ${folderContext}`;
|
|
1058
|
+
if (normalized.startsWith(prefix)) {
|
|
1059
|
+
const remainder = normalized.slice(prefix.length);
|
|
1060
|
+
if (/^(?:\r?\n){2}/.test(remainder)) {
|
|
1061
|
+
normalized = remainder.replace(/^(?:\r?\n){2}/, "");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return normalized.replace(/^@@ -\d+(?:,\d+)?(?: \+\d+(?:,\d+)?)? @@ \(\d+ before, \d+ after\)(?:\r?\n){1,2}/, "");
|
|
892
1066
|
}
|
|
893
1067
|
function stripAnsi(text) {
|
|
894
1068
|
// qmd may emit spinners/progress bars even with --json, especially on first model download.
|
|
@@ -916,11 +1090,11 @@ function parseQmdJson(stdout) {
|
|
|
916
1090
|
return [];
|
|
917
1091
|
return JSON.parse(jsonText);
|
|
918
1092
|
}
|
|
919
|
-
export function runQmdSearch(mode, query, limit) {
|
|
1093
|
+
export function runQmdSearch(mode, query, limit, options = {}) {
|
|
920
1094
|
const subcommand = mode === "keyword" ? "search" : mode === "semantic" ? "vsearch" : "query";
|
|
921
1095
|
const args = [subcommand, "--json", "-c", QMD_COLLECTION_NAME, "-n", String(limit), query];
|
|
922
1096
|
return new Promise((resolve, reject) => {
|
|
923
|
-
execFileFn("qmd", args, { timeout: 60_000 }, (err, stdout, stderr) => {
|
|
1097
|
+
execFileFn("qmd", args, { timeout: 60_000, signal: options.signal }, (err, stdout, stderr) => {
|
|
924
1098
|
if (err) {
|
|
925
1099
|
reject(new Error(stderr?.trim() || err.message));
|
|
926
1100
|
return;
|
|
@@ -940,6 +1114,41 @@ export function runQmdSearch(mode, query, limit) {
|
|
|
940
1114
|
});
|
|
941
1115
|
});
|
|
942
1116
|
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Best-effort check of whether vector embeddings are actually usable for
|
|
1119
|
+
* semantic/deep search right now. Runs a tiny semantic probe and looks for
|
|
1120
|
+
* qmd's "need embeddings" warning. Bounded by a short timeout because the very
|
|
1121
|
+
* first semantic query can trigger a model download — returns "unknown" rather
|
|
1122
|
+
* than blocking on it. "ready" means the probe ran without the warning; it does
|
|
1123
|
+
* not prove the index has content.
|
|
1124
|
+
*/
|
|
1125
|
+
export async function probeEmbeddings() {
|
|
1126
|
+
let timer;
|
|
1127
|
+
// Abort the underlying qmd child when the timeout fires so it does not keep
|
|
1128
|
+
// the event loop open until its own 60s timeout and hang the CLI.
|
|
1129
|
+
const controller = new AbortController();
|
|
1130
|
+
try {
|
|
1131
|
+
const { stderr } = await Promise.race([
|
|
1132
|
+
runQmdSearch("semantic", "memory", 1, { signal: controller.signal }),
|
|
1133
|
+
new Promise((_, reject) => {
|
|
1134
|
+
timer = setTimeout(() => {
|
|
1135
|
+
controller.abort();
|
|
1136
|
+
reject(new Error("timeout"));
|
|
1137
|
+
}, 4_000);
|
|
1138
|
+
}),
|
|
1139
|
+
]);
|
|
1140
|
+
return /need embeddings/i.test(stderr ?? "") ? "missing" : "ready";
|
|
1141
|
+
}
|
|
1142
|
+
catch (err) {
|
|
1143
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1144
|
+
if (/need embeddings/i.test(msg))
|
|
1145
|
+
return "missing";
|
|
1146
|
+
return "unknown";
|
|
1147
|
+
}
|
|
1148
|
+
finally {
|
|
1149
|
+
clearTimeout(timer);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
943
1152
|
export async function memoryWrite(params) {
|
|
944
1153
|
ensureDirs();
|
|
945
1154
|
const target = params.target ?? "daily";
|
|
@@ -949,17 +1158,18 @@ export async function memoryWrite(params) {
|
|
|
949
1158
|
if (target === "daily") {
|
|
950
1159
|
const filePath = dailyPath(todayStr());
|
|
951
1160
|
const existing = readFileSafe(filePath) ?? "";
|
|
952
|
-
const
|
|
1161
|
+
const safeExisting = redactSecrets(existing).content;
|
|
1162
|
+
const existingPreview = buildPreview(safeExisting, {
|
|
953
1163
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
954
1164
|
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
955
1165
|
mode: "end",
|
|
956
1166
|
});
|
|
957
1167
|
const existingSnippet = existingPreview.preview
|
|
958
|
-
? `\n\n${formatPreviewBlock("Existing daily log preview",
|
|
1168
|
+
? `\n\n${formatPreviewBlock("Existing daily log preview", safeExisting, "end")}`
|
|
959
1169
|
: "\n\nDaily log was empty.";
|
|
960
1170
|
const separator = existing.trim() ? "\n\n" : "";
|
|
961
|
-
const
|
|
962
|
-
fs.writeFileSync(filePath, existing + separator +
|
|
1171
|
+
const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
|
|
1172
|
+
fs.writeFileSync(filePath, existing + separator + stored.entry, "utf-8");
|
|
963
1173
|
await ensureQmdAvailableForUpdate();
|
|
964
1174
|
scheduleQmdUpdate();
|
|
965
1175
|
return {
|
|
@@ -970,6 +1180,8 @@ export async function memoryWrite(params) {
|
|
|
970
1180
|
mode: "append",
|
|
971
1181
|
sessionId: sid,
|
|
972
1182
|
timestamp: ts,
|
|
1183
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1184
|
+
redacted: stored.redacted,
|
|
973
1185
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
974
1186
|
existingPreview,
|
|
975
1187
|
},
|
|
@@ -986,20 +1198,21 @@ export async function memoryWrite(params) {
|
|
|
986
1198
|
}
|
|
987
1199
|
const filePath = topicPath(slug);
|
|
988
1200
|
const existing = readFileSafe(filePath) ?? "";
|
|
989
|
-
const
|
|
1201
|
+
const safeExisting = redactSecrets(existing).content;
|
|
1202
|
+
const existingPreview = buildPreview(safeExisting, {
|
|
990
1203
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
991
1204
|
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
992
1205
|
mode: "end",
|
|
993
1206
|
});
|
|
994
1207
|
const existingSnippet = existingPreview.preview
|
|
995
|
-
? `\n\n${formatPreviewBlock("Existing topic preview",
|
|
1208
|
+
? `\n\n${formatPreviewBlock("Existing topic preview", safeExisting, "end")}`
|
|
996
1209
|
: "\n\nTopic file was empty.";
|
|
997
1210
|
const linkDate = params.date?.trim() || todayStr();
|
|
998
1211
|
const header = `# Topic: ${topic}\n\n<!-- created: ${ts} [${sid}] -->\n`;
|
|
999
1212
|
const separator = existing.trim() ? "\n\n" : "";
|
|
1000
1213
|
const base = existing.trim() ? existing : header.trimEnd();
|
|
1001
|
-
const
|
|
1002
|
-
fs.writeFileSync(filePath, `${base}${separator}${
|
|
1214
|
+
const stored = formatStoredEntry(`${content.trim()}\nDaily: [[${linkDate}]]`, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
|
|
1215
|
+
fs.writeFileSync(filePath, `${base}${separator}${stored.entry}`, "utf-8");
|
|
1003
1216
|
await ensureQmdAvailableForUpdate();
|
|
1004
1217
|
scheduleQmdUpdate();
|
|
1005
1218
|
return {
|
|
@@ -1013,6 +1226,8 @@ export async function memoryWrite(params) {
|
|
|
1013
1226
|
topic,
|
|
1014
1227
|
slug,
|
|
1015
1228
|
date: linkDate,
|
|
1229
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1230
|
+
redacted: stored.redacted,
|
|
1016
1231
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1017
1232
|
existingPreview,
|
|
1018
1233
|
},
|
|
@@ -1021,17 +1236,18 @@ export async function memoryWrite(params) {
|
|
|
1021
1236
|
// long_term
|
|
1022
1237
|
const memFile = getMemoryFile();
|
|
1023
1238
|
const existing = readFileSafe(memFile) ?? "";
|
|
1024
|
-
const
|
|
1239
|
+
const safeExisting = redactSecrets(existing).content;
|
|
1240
|
+
const existingPreview = buildPreview(safeExisting, {
|
|
1025
1241
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
1026
1242
|
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
1027
1243
|
mode: "middle",
|
|
1028
1244
|
});
|
|
1029
1245
|
const existingSnippet = existingPreview.preview
|
|
1030
|
-
? `\n\n${formatPreviewBlock("Existing MEMORY.md preview",
|
|
1246
|
+
? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", safeExisting, "middle")}`
|
|
1031
1247
|
: "\n\nMEMORY.md was empty.";
|
|
1032
1248
|
if (mode === "overwrite") {
|
|
1033
|
-
const
|
|
1034
|
-
fs.writeFileSync(memFile,
|
|
1249
|
+
const stored = formatStoredEntry(content, `<!-- last updated: ${ts} [${sid}] -->`, params.sourceUri);
|
|
1250
|
+
fs.writeFileSync(memFile, stored.entry, "utf-8");
|
|
1035
1251
|
await ensureQmdAvailableForUpdate();
|
|
1036
1252
|
scheduleQmdUpdate();
|
|
1037
1253
|
return {
|
|
@@ -1042,6 +1258,8 @@ export async function memoryWrite(params) {
|
|
|
1042
1258
|
mode: "overwrite",
|
|
1043
1259
|
sessionId: sid,
|
|
1044
1260
|
timestamp: ts,
|
|
1261
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1262
|
+
redacted: stored.redacted,
|
|
1045
1263
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1046
1264
|
existingPreview,
|
|
1047
1265
|
},
|
|
@@ -1049,8 +1267,8 @@ export async function memoryWrite(params) {
|
|
|
1049
1267
|
}
|
|
1050
1268
|
// append (default)
|
|
1051
1269
|
const separator = existing.trim() ? "\n\n" : "";
|
|
1052
|
-
const
|
|
1053
|
-
fs.writeFileSync(memFile, existing + separator +
|
|
1270
|
+
const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
|
|
1271
|
+
fs.writeFileSync(memFile, existing + separator + stored.entry, "utf-8");
|
|
1054
1272
|
await ensureQmdAvailableForUpdate();
|
|
1055
1273
|
scheduleQmdUpdate();
|
|
1056
1274
|
return {
|
|
@@ -1061,6 +1279,8 @@ export async function memoryWrite(params) {
|
|
|
1061
1279
|
mode: "append",
|
|
1062
1280
|
sessionId: sid,
|
|
1063
1281
|
timestamp: ts,
|
|
1282
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1283
|
+
redacted: stored.redacted,
|
|
1064
1284
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1065
1285
|
existingPreview,
|
|
1066
1286
|
},
|
|
@@ -1073,7 +1293,11 @@ export async function scratchpadAction(params) {
|
|
|
1073
1293
|
const ts = nowTimestamp();
|
|
1074
1294
|
const spFile = getScratchpadFile();
|
|
1075
1295
|
const existing = readFileSafe(spFile) ?? "";
|
|
1076
|
-
let items = parseScratchpad(existing)
|
|
1296
|
+
let items = parseScratchpad(existing).map((item) => ({
|
|
1297
|
+
...item,
|
|
1298
|
+
text: redactSecrets(item.text).content,
|
|
1299
|
+
meta: redactSecrets(item.meta).content,
|
|
1300
|
+
}));
|
|
1077
1301
|
if (action === "list") {
|
|
1078
1302
|
if (items.length === 0) {
|
|
1079
1303
|
return { text: "Scratchpad is empty.", details: {} };
|
|
@@ -1097,7 +1321,8 @@ export async function scratchpadAction(params) {
|
|
|
1097
1321
|
if (!text) {
|
|
1098
1322
|
return { text: "Error: 'text' is required for add.", details: {} };
|
|
1099
1323
|
}
|
|
1100
|
-
|
|
1324
|
+
const safeText = redactSecrets(text).content;
|
|
1325
|
+
items.push({ done: false, text: safeText, meta: `<!-- ${ts} [${sid}] -->` });
|
|
1101
1326
|
const serialized = serializeScratchpad(items);
|
|
1102
1327
|
const preview = buildPreview(serialized, {
|
|
1103
1328
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
@@ -1108,7 +1333,7 @@ export async function scratchpadAction(params) {
|
|
|
1108
1333
|
await ensureQmdAvailableForUpdate();
|
|
1109
1334
|
scheduleQmdUpdate();
|
|
1110
1335
|
return {
|
|
1111
|
-
text: `Added: - [ ] ${
|
|
1336
|
+
text: `Added: - [ ] ${safeText}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
|
|
1112
1337
|
details: {
|
|
1113
1338
|
action,
|
|
1114
1339
|
sessionId: sid,
|
|
@@ -1526,7 +1751,10 @@ export async function distilMemories(params) {
|
|
|
1526
1751
|
const content = readFileSafe(path.join(DAILY_DIR, file));
|
|
1527
1752
|
if (!content?.trim())
|
|
1528
1753
|
continue;
|
|
1529
|
-
|
|
1754
|
+
const safeContent = filterMemoryForContext(content);
|
|
1755
|
+
if (!safeContent)
|
|
1756
|
+
continue;
|
|
1757
|
+
allEntries.push(...parseDailyEntries(date, safeContent));
|
|
1530
1758
|
}
|
|
1531
1759
|
const topicEntriesByTopic = new Map();
|
|
1532
1760
|
let totalTopicEntries = 0;
|
|
@@ -1535,9 +1763,12 @@ export async function distilMemories(params) {
|
|
|
1535
1763
|
const content = readFileSafe(path.join(TOPICS_DIR, file));
|
|
1536
1764
|
if (!content?.trim())
|
|
1537
1765
|
continue;
|
|
1538
|
-
const
|
|
1766
|
+
const safeContent = filterMemoryForContext(content);
|
|
1767
|
+
if (!safeContent)
|
|
1768
|
+
continue;
|
|
1769
|
+
const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
|
|
1539
1770
|
const title = titleMatch?.[1]?.trim() || slug;
|
|
1540
|
-
const entries = parseTopicEntries(title, slug,
|
|
1771
|
+
const entries = parseTopicEntries(title, slug, safeContent);
|
|
1541
1772
|
if (entries.length === 0)
|
|
1542
1773
|
continue;
|
|
1543
1774
|
totalTopicEntries += entries.length;
|
|
@@ -1584,7 +1815,8 @@ export async function distilMemories(params) {
|
|
|
1584
1815
|
let pinnedSection = "";
|
|
1585
1816
|
const existingMemory = readFileSafe(MEMORY_FILE);
|
|
1586
1817
|
if (existingMemory) {
|
|
1587
|
-
const
|
|
1818
|
+
const safeExistingMemory = filterMemoryForContext(existingMemory);
|
|
1819
|
+
const pinnedMatch = safeExistingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
|
|
1588
1820
|
if (pinnedMatch) {
|
|
1589
1821
|
pinnedSection = pinnedMatch[1].trim();
|
|
1590
1822
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagentmemory",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.13",
|
|
4
|
+
"description": "agentmemory (agent-memory) is persistent memory for coding agents (Claude Code, OpenAI 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",
|
|
7
7
|
"exports": {
|
|
@@ -11,37 +11,51 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"agent-memory": "./dist/
|
|
14
|
+
"agent-memory": "./dist/cli.js"
|
|
15
15
|
},
|
|
16
16
|
"type": "module",
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=20"
|
|
19
|
+
},
|
|
17
20
|
"keywords": [
|
|
21
|
+
"agentmemory",
|
|
22
|
+
"agent-memory",
|
|
23
|
+
"agent memory",
|
|
24
|
+
"ai-memory",
|
|
25
|
+
"llm-memory",
|
|
18
26
|
"memory",
|
|
19
27
|
"search",
|
|
20
28
|
"qmd",
|
|
29
|
+
"semantic-search",
|
|
21
30
|
"scratchpad",
|
|
22
31
|
"daily-log",
|
|
23
32
|
"claude-code",
|
|
33
|
+
"openai-codex",
|
|
24
34
|
"codex",
|
|
25
35
|
"cursor",
|
|
26
36
|
"agent",
|
|
27
|
-
"agent
|
|
28
|
-
"
|
|
37
|
+
"coding-agent",
|
|
38
|
+
"developer-tools",
|
|
39
|
+
"knowledge-management"
|
|
29
40
|
],
|
|
30
41
|
"author": "jayzeng",
|
|
31
42
|
"license": "MIT",
|
|
32
43
|
"repository": {
|
|
33
44
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/jayzeng/
|
|
45
|
+
"url": "git+https://github.com/jayzeng/agentmemory.git"
|
|
35
46
|
},
|
|
36
47
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/jayzeng/
|
|
48
|
+
"url": "https://github.com/jayzeng/agentmemory/issues"
|
|
38
49
|
},
|
|
39
|
-
"homepage": "https://github.com/jayzeng/
|
|
50
|
+
"homepage": "https://github.com/jayzeng/agentmemory#readme",
|
|
40
51
|
"files": [
|
|
41
52
|
"src",
|
|
42
53
|
"skills",
|
|
43
54
|
"scripts",
|
|
44
|
-
"dist",
|
|
55
|
+
"dist/cli.d.ts",
|
|
56
|
+
"dist/cli.js",
|
|
57
|
+
"dist/core.d.ts",
|
|
58
|
+
"dist/core.js",
|
|
45
59
|
"README.md",
|
|
46
60
|
"LICENSE"
|
|
47
61
|
],
|
|
@@ -50,18 +64,22 @@
|
|
|
50
64
|
},
|
|
51
65
|
"scripts": {
|
|
52
66
|
"postinstall": "node scripts/postinstall.cjs",
|
|
53
|
-
"build": "tsc -p tsconfig.json --noEmit",
|
|
67
|
+
"build": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.eval.json --noEmit",
|
|
68
|
+
"build:eval": "tsc -p tsconfig.eval.json --noEmit",
|
|
54
69
|
"build:lib": "tsc -p tsconfig.build.json",
|
|
55
70
|
"build:cli": "bun build src/cli.ts --compile --outfile dist/agent-memory --define __VERSION__=\"'$(node -p \"require('./package.json').version\")'\"",
|
|
56
|
-
"
|
|
71
|
+
"eval:feedback": "bun eval/run.ts",
|
|
72
|
+
"prepack": "npm run build:lib",
|
|
57
73
|
"lint": "biome check .",
|
|
58
74
|
"test": "bun test test/unit.test.ts",
|
|
59
75
|
"test:unit": "bun test test/unit.test.ts",
|
|
60
76
|
"test:cli": "bun test test/cli.test.ts",
|
|
77
|
+
"test:eval": "bun test test/eval.test.ts",
|
|
61
78
|
"install-skills": "bash scripts/install-skills.sh"
|
|
62
79
|
},
|
|
63
80
|
"devDependencies": {
|
|
64
81
|
"@biomejs/biome": "^2.4.0",
|
|
82
|
+
"@types/bun": "^1.3.14",
|
|
65
83
|
"@types/node": "^25.3.0",
|
|
66
84
|
"tsx": "^4.0.0",
|
|
67
85
|
"typescript": "^5.9.3"
|