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/README.md +83 -38
- package/dist/cli.js +60 -67
- package/dist/core.d.ts +21 -1
- package/dist/core.js +275 -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 +290 -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;
|
|
@@ -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) =>
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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",
|
|
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
|
|
969
|
-
fs.writeFileSync(filePath, existing + separator +
|
|
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
|
|
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",
|
|
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
|
|
1009
|
-
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");
|
|
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
|
|
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",
|
|
1246
|
+
? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", safeExisting, "middle")}`
|
|
1038
1247
|
: "\n\nMEMORY.md was empty.";
|
|
1039
1248
|
if (mode === "overwrite") {
|
|
1040
|
-
const
|
|
1041
|
-
fs.writeFileSync(memFile,
|
|
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
|
|
1060
|
-
fs.writeFileSync(memFile, existing + separator +
|
|
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
|
-
|
|
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: - [ ] ${
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Install (or uninstall) agent-memory skills for Claude Code, Codex, and
|
|
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
|