myagentmemory 0.4.11 → 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/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("<!--", "&lt;!--"));
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
- if (todayContent?.trim()) {
267
- const section = formatContextSection(`## Daily log: ${today} (today)`, todayContent, "end", CONTEXT_DAILY_MAX_LINES, CONTEXT_DAILY_MAX_CHARS);
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
- if (searchResults?.trim()) {
272
- const section = formatContextSection("## Relevant memories (auto-retrieved)", searchResults, "start", CONTEXT_SEARCH_MAX_LINES, CONTEXT_SEARCH_MAX_CHARS);
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
- if (longTerm?.trim()) {
278
- const section = formatContextSection("## MEMORY.md (long-term)", longTerm, "middle", CONTEXT_LONG_TERM_MAX_LINES, CONTEXT_LONG_TERM_MAX_CHARS);
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
- if (yesterdayContent?.trim()) {
284
- const section = formatContextSection(`## Daily log: ${yesterday} (yesterday)`, yesterdayContent, "end", CONTEXT_DAILY_MAX_LINES, CONTEXT_DAILY_MAX_CHARS);
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 result = buildPreview(context, {
294
- maxLines: Number.POSITIVE_INFINITY,
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
- if (!content?.trim())
413
+ const safeContent = content ? filterMemoryForContext(content) : "";
414
+ if (!safeContent)
323
415
  continue;
324
- const titleMatch = content.match(/^# Topic:\s*(.+)$/m);
416
+ const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
325
417
  const title = titleMatch?.[1]?.trim() || slug;
326
- entries.push(...parseTopicEntries(title, slug, content));
418
+ entries.push(...parseTopicEntries(title, slug, safeContent));
327
419
  }
328
420
  if (entries.length === 0)
329
421
  return null;
@@ -851,10 +943,63 @@ export async function getQmdHealth() {
851
943
  });
852
944
  });
853
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
+ }
854
997
  /** Search for memories relevant to the user's prompt. Returns formatted markdown or empty string on error. */
855
998
  export async function searchRelevantMemories(prompt) {
856
999
  if (!qmdAvailable || !prompt.trim())
857
1000
  return "";
1001
+ let timer;
1002
+ const controller = new AbortController();
858
1003
  // Sanitize: strip control chars, limit to 200 chars for the search query
859
1004
  const sanitized = prompt
860
1005
  // biome-ignore lint/suspicious/noControlCharactersInRegex: we intentionally strip control chars.
@@ -868,19 +1013,26 @@ export async function searchRelevantMemories(prompt) {
868
1013
  if (!hasCollection)
869
1014
  return "";
870
1015
  const results = await Promise.race([
871
- runQmdSearch("keyword", sanitized, 3),
872
- new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3_000)),
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
+ }),
873
1023
  ]);
874
1024
  if (!results || results.results.length === 0)
875
1025
  return "";
876
1026
  const snippets = results.results
877
1027
  .map((r) => {
878
- const text = getQmdResultText(r);
879
- if (!text.trim())
1028
+ const text = filterMemoryForContext(getQmdResultText(r));
1029
+ if (!text)
880
1030
  return null;
881
1031
  const filePath = getQmdResultPath(r);
1032
+ if (!qmdResultPassesSourcePolicy(filePath, text))
1033
+ return null;
882
1034
  const filePart = filePath ? `_${filePath}_` : "";
883
- return filePart ? `${filePart}\n${text.trim()}` : text.trim();
1035
+ return filePart ? `${filePart}\n${text}` : text;
884
1036
  })
885
1037
  .filter(Boolean);
886
1038
  if (snippets.length === 0)
@@ -890,12 +1042,27 @@ export async function searchRelevantMemories(prompt) {
890
1042
  catch {
891
1043
  return "";
892
1044
  }
1045
+ finally {
1046
+ clearTimeout(timer);
1047
+ }
893
1048
  }
894
1049
  export function getQmdResultPath(r) {
895
1050
  return r.path ?? r.file;
896
1051
  }
897
1052
  export function getQmdResultText(r) {
898
- return r.content ?? r.chunk ?? r.snippet ?? "";
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}/, "");
899
1066
  }
900
1067
  function stripAnsi(text) {
901
1068
  // qmd may emit spinners/progress bars even with --json, especially on first model download.
@@ -923,11 +1090,11 @@ function parseQmdJson(stdout) {
923
1090
  return [];
924
1091
  return JSON.parse(jsonText);
925
1092
  }
926
- export function runQmdSearch(mode, query, limit) {
1093
+ export function runQmdSearch(mode, query, limit, options = {}) {
927
1094
  const subcommand = mode === "keyword" ? "search" : mode === "semantic" ? "vsearch" : "query";
928
1095
  const args = [subcommand, "--json", "-c", QMD_COLLECTION_NAME, "-n", String(limit), query];
929
1096
  return new Promise((resolve, reject) => {
930
- execFileFn("qmd", args, { timeout: 60_000 }, (err, stdout, stderr) => {
1097
+ execFileFn("qmd", args, { timeout: 60_000, signal: options.signal }, (err, stdout, stderr) => {
931
1098
  if (err) {
932
1099
  reject(new Error(stderr?.trim() || err.message));
933
1100
  return;
@@ -947,6 +1114,41 @@ export function runQmdSearch(mode, query, limit) {
947
1114
  });
948
1115
  });
949
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
+ }
950
1152
  export async function memoryWrite(params) {
951
1153
  ensureDirs();
952
1154
  const target = params.target ?? "daily";
@@ -956,17 +1158,18 @@ export async function memoryWrite(params) {
956
1158
  if (target === "daily") {
957
1159
  const filePath = dailyPath(todayStr());
958
1160
  const existing = readFileSafe(filePath) ?? "";
959
- const existingPreview = buildPreview(existing, {
1161
+ const safeExisting = redactSecrets(existing).content;
1162
+ const existingPreview = buildPreview(safeExisting, {
960
1163
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
961
1164
  maxChars: RESPONSE_PREVIEW_MAX_CHARS,
962
1165
  mode: "end",
963
1166
  });
964
1167
  const existingSnippet = existingPreview.preview
965
- ? `\n\n${formatPreviewBlock("Existing daily log preview", existing, "end")}`
1168
+ ? `\n\n${formatPreviewBlock("Existing daily log preview", safeExisting, "end")}`
966
1169
  : "\n\nDaily log was empty.";
967
1170
  const separator = existing.trim() ? "\n\n" : "";
968
- const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
969
- fs.writeFileSync(filePath, existing + separator + stamped, "utf-8");
1171
+ const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
1172
+ fs.writeFileSync(filePath, existing + separator + stored.entry, "utf-8");
970
1173
  await ensureQmdAvailableForUpdate();
971
1174
  scheduleQmdUpdate();
972
1175
  return {
@@ -977,6 +1180,8 @@ export async function memoryWrite(params) {
977
1180
  mode: "append",
978
1181
  sessionId: sid,
979
1182
  timestamp: ts,
1183
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1184
+ redacted: stored.redacted,
980
1185
  qmdUpdateMode: getQmdUpdateMode(),
981
1186
  existingPreview,
982
1187
  },
@@ -993,20 +1198,21 @@ export async function memoryWrite(params) {
993
1198
  }
994
1199
  const filePath = topicPath(slug);
995
1200
  const existing = readFileSafe(filePath) ?? "";
996
- const existingPreview = buildPreview(existing, {
1201
+ const safeExisting = redactSecrets(existing).content;
1202
+ const existingPreview = buildPreview(safeExisting, {
997
1203
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
998
1204
  maxChars: RESPONSE_PREVIEW_MAX_CHARS,
999
1205
  mode: "end",
1000
1206
  });
1001
1207
  const existingSnippet = existingPreview.preview
1002
- ? `\n\n${formatPreviewBlock("Existing topic preview", existing, "end")}`
1208
+ ? `\n\n${formatPreviewBlock("Existing topic preview", safeExisting, "end")}`
1003
1209
  : "\n\nTopic file was empty.";
1004
1210
  const linkDate = params.date?.trim() || todayStr();
1005
1211
  const header = `# Topic: ${topic}\n\n<!-- created: ${ts} [${sid}] -->\n`;
1006
1212
  const separator = existing.trim() ? "\n\n" : "";
1007
1213
  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");
1214
+ const stored = formatStoredEntry(`${content.trim()}\nDaily: [[${linkDate}]]`, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
1215
+ fs.writeFileSync(filePath, `${base}${separator}${stored.entry}`, "utf-8");
1010
1216
  await ensureQmdAvailableForUpdate();
1011
1217
  scheduleQmdUpdate();
1012
1218
  return {
@@ -1020,6 +1226,8 @@ export async function memoryWrite(params) {
1020
1226
  topic,
1021
1227
  slug,
1022
1228
  date: linkDate,
1229
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1230
+ redacted: stored.redacted,
1023
1231
  qmdUpdateMode: getQmdUpdateMode(),
1024
1232
  existingPreview,
1025
1233
  },
@@ -1028,17 +1236,18 @@ export async function memoryWrite(params) {
1028
1236
  // long_term
1029
1237
  const memFile = getMemoryFile();
1030
1238
  const existing = readFileSafe(memFile) ?? "";
1031
- const existingPreview = buildPreview(existing, {
1239
+ const safeExisting = redactSecrets(existing).content;
1240
+ const existingPreview = buildPreview(safeExisting, {
1032
1241
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
1033
1242
  maxChars: RESPONSE_PREVIEW_MAX_CHARS,
1034
1243
  mode: "middle",
1035
1244
  });
1036
1245
  const existingSnippet = existingPreview.preview
1037
- ? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", existing, "middle")}`
1246
+ ? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", safeExisting, "middle")}`
1038
1247
  : "\n\nMEMORY.md was empty.";
1039
1248
  if (mode === "overwrite") {
1040
- const stamped = `<!-- last updated: ${ts} [${sid}] -->\n${content}`;
1041
- fs.writeFileSync(memFile, stamped, "utf-8");
1249
+ const stored = formatStoredEntry(content, `<!-- last updated: ${ts} [${sid}] -->`, params.sourceUri);
1250
+ fs.writeFileSync(memFile, stored.entry, "utf-8");
1042
1251
  await ensureQmdAvailableForUpdate();
1043
1252
  scheduleQmdUpdate();
1044
1253
  return {
@@ -1049,6 +1258,8 @@ export async function memoryWrite(params) {
1049
1258
  mode: "overwrite",
1050
1259
  sessionId: sid,
1051
1260
  timestamp: ts,
1261
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1262
+ redacted: stored.redacted,
1052
1263
  qmdUpdateMode: getQmdUpdateMode(),
1053
1264
  existingPreview,
1054
1265
  },
@@ -1056,8 +1267,8 @@ export async function memoryWrite(params) {
1056
1267
  }
1057
1268
  // append (default)
1058
1269
  const separator = existing.trim() ? "\n\n" : "";
1059
- const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
1060
- fs.writeFileSync(memFile, existing + separator + stamped, "utf-8");
1270
+ const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
1271
+ fs.writeFileSync(memFile, existing + separator + stored.entry, "utf-8");
1061
1272
  await ensureQmdAvailableForUpdate();
1062
1273
  scheduleQmdUpdate();
1063
1274
  return {
@@ -1068,6 +1279,8 @@ export async function memoryWrite(params) {
1068
1279
  mode: "append",
1069
1280
  sessionId: sid,
1070
1281
  timestamp: ts,
1282
+ sourceUri: sanitizeSourceUri(params.sourceUri),
1283
+ redacted: stored.redacted,
1071
1284
  qmdUpdateMode: getQmdUpdateMode(),
1072
1285
  existingPreview,
1073
1286
  },
@@ -1080,7 +1293,11 @@ export async function scratchpadAction(params) {
1080
1293
  const ts = nowTimestamp();
1081
1294
  const spFile = getScratchpadFile();
1082
1295
  const existing = readFileSafe(spFile) ?? "";
1083
- 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
+ }));
1084
1301
  if (action === "list") {
1085
1302
  if (items.length === 0) {
1086
1303
  return { text: "Scratchpad is empty.", details: {} };
@@ -1104,7 +1321,8 @@ export async function scratchpadAction(params) {
1104
1321
  if (!text) {
1105
1322
  return { text: "Error: 'text' is required for add.", details: {} };
1106
1323
  }
1107
- items.push({ done: false, text, meta: `<!-- ${ts} [${sid}] -->` });
1324
+ const safeText = redactSecrets(text).content;
1325
+ items.push({ done: false, text: safeText, meta: `<!-- ${ts} [${sid}] -->` });
1108
1326
  const serialized = serializeScratchpad(items);
1109
1327
  const preview = buildPreview(serialized, {
1110
1328
  maxLines: RESPONSE_PREVIEW_MAX_LINES,
@@ -1115,7 +1333,7 @@ export async function scratchpadAction(params) {
1115
1333
  await ensureQmdAvailableForUpdate();
1116
1334
  scheduleQmdUpdate();
1117
1335
  return {
1118
- text: `Added: - [ ] ${text}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
1336
+ text: `Added: - [ ] ${safeText}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
1119
1337
  details: {
1120
1338
  action,
1121
1339
  sessionId: sid,
@@ -1533,7 +1751,10 @@ export async function distilMemories(params) {
1533
1751
  const content = readFileSafe(path.join(DAILY_DIR, file));
1534
1752
  if (!content?.trim())
1535
1753
  continue;
1536
- allEntries.push(...parseDailyEntries(date, content));
1754
+ const safeContent = filterMemoryForContext(content);
1755
+ if (!safeContent)
1756
+ continue;
1757
+ allEntries.push(...parseDailyEntries(date, safeContent));
1537
1758
  }
1538
1759
  const topicEntriesByTopic = new Map();
1539
1760
  let totalTopicEntries = 0;
@@ -1542,9 +1763,12 @@ export async function distilMemories(params) {
1542
1763
  const content = readFileSafe(path.join(TOPICS_DIR, file));
1543
1764
  if (!content?.trim())
1544
1765
  continue;
1545
- const titleMatch = content.match(/^# Topic:\s*(.+)$/m);
1766
+ const safeContent = filterMemoryForContext(content);
1767
+ if (!safeContent)
1768
+ continue;
1769
+ const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
1546
1770
  const title = titleMatch?.[1]?.trim() || slug;
1547
- const entries = parseTopicEntries(title, slug, content);
1771
+ const entries = parseTopicEntries(title, slug, safeContent);
1548
1772
  if (entries.length === 0)
1549
1773
  continue;
1550
1774
  totalTopicEntries += entries.length;
@@ -1591,7 +1815,8 @@ export async function distilMemories(params) {
1591
1815
  let pinnedSection = "";
1592
1816
  const existingMemory = readFileSafe(MEMORY_FILE);
1593
1817
  if (existingMemory) {
1594
- const pinnedMatch = existingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
1818
+ const safeExistingMemory = filterMemoryForContext(existingMemory);
1819
+ const pinnedMatch = safeExistingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
1595
1820
  if (pinnedMatch) {
1596
1821
  pinnedSection = pinnedMatch[1].trim();
1597
1822
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "myagentmemory",
3
- "version": "0.4.11",
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",
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/agent-memory"
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-memory",
28
- "coding-agent"
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/agent-memory.git"
45
+ "url": "git+https://github.com/jayzeng/agentmemory.git"
35
46
  },
36
47
  "bugs": {
37
- "url": "https://github.com/jayzeng/agent-memory/issues"
48
+ "url": "https://github.com/jayzeng/agentmemory/issues"
38
49
  },
39
- "homepage": "https://github.com/jayzeng/agent-memory#readme",
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
- "prepack": "npm run build:lib && bun run build:cli",
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"
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Install (or uninstall) agent-memory skills for Claude Code, Codex, and Cursor.
2
+ # Install (or uninstall) agent-memory skills for Claude Code, Codex, Cursor, and Agent CLI.
3
3
  # Usage: bash scripts/install-skills.sh [--uninstall]
4
4
 
5
5
  set -euo pipefail
@@ -62,11 +62,13 @@ SKILL_DIRS=(
62
62
  "$HOME/.claude/skills/agent-memory"
63
63
  "$HOME/.codex/skills/agent-memory"
64
64
  "$HOME/.cursor/skills/agent-memory"
65
+ "$HOME/.agents/skills/agent-memory"
65
66
  )
66
67
  SKILL_LABELS=(
67
68
  "Claude Code skill"
68
69
  "Codex skill"
69
70
  "Cursor skill"
71
+ "Agent CLI skill"
70
72
  )
71
73
 
72
74
  if $UNINSTALL; then
@@ -81,6 +83,7 @@ else
81
83
  install_skill "Claude Code skill" "$PROJECT_DIR/skills/claude-code" "$HOME/.claude/skills/agent-memory" "$HOME/.claude" '[ -f "$HOME/.claude/settings.json" ] || [ -f "$HOME/.claude/settings.local.json" ] || command_exists claude'
82
84
  install_skill "Codex skill" "$PROJECT_DIR/skills/codex" "$HOME/.codex/skills/agent-memory" "$HOME/.codex" '[ -f "$HOME/.codex/config.toml" ] || command_exists codex'
83
85
  install_skill "Cursor skill" "$PROJECT_DIR/skills/cursor" "$HOME/.cursor/skills/agent-memory" "$HOME/.cursor"
86
+ install_skill "Agent CLI skill" "$PROJECT_DIR/skills/agent" "$HOME/.agents/skills/agent-memory" "$HOME/.agents"
84
87
  echo ""
85
88
  echo "Done."
86
89
  fi