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/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;
@@ -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) => 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
+ }),
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.trim())
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.trim()}` : text.trim();
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
- 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}/, "");
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 existingPreview = buildPreview(existing, {
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", existing, "end")}`
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 stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
962
- 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");
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 existingPreview = buildPreview(existing, {
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", existing, "end")}`
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 stamped = `<!-- ${ts} [${sid}] -->\n${content.trim()}\nDaily: [[${linkDate}]]`;
1002
- 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");
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 existingPreview = buildPreview(existing, {
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", existing, "middle")}`
1246
+ ? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", safeExisting, "middle")}`
1031
1247
  : "\n\nMEMORY.md was empty.";
1032
1248
  if (mode === "overwrite") {
1033
- const stamped = `<!-- last updated: ${ts} [${sid}] -->\n${content}`;
1034
- 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");
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 stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
1053
- 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");
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
- items.push({ done: false, text, meta: `<!-- ${ts} [${sid}] -->` });
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: - [ ] ${text}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
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
- allEntries.push(...parseDailyEntries(date, content));
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 titleMatch = content.match(/^# Topic:\s*(.+)$/m);
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, content);
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 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# |$)/);
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.12",
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"