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/src/core.ts
CHANGED
|
@@ -152,6 +152,114 @@ function normalizeContent(content: string): string {
|
|
|
152
152
|
return content.trim();
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
const SECRET_PATTERNS: RegExp[] = [
|
|
156
|
+
/\bsk-[A-Za-z0-9_-]{16,}\b/g,
|
|
157
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
158
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
159
|
+
/\b(?:api[_-]?key|access[_-]?token|secret)\s*[:=]\s*["']?[A-Za-z0-9_./+=-]{16,}["']?/gi,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
/** Redact common credential shapes before content reaches disk or agent context. */
|
|
163
|
+
export function redactSecrets(content: string): { content: string; redacted: boolean } {
|
|
164
|
+
let redactedContent = content;
|
|
165
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
166
|
+
redactedContent = redactedContent.replace(pattern, "[REDACTED_SECRET]");
|
|
167
|
+
}
|
|
168
|
+
return { content: redactedContent, redacted: redactedContent !== content };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isInactiveMemoryEntry(entry: string, now: Date): boolean {
|
|
172
|
+
const metadataHeader = entry.split(/\n\s*\n/, 1)[0];
|
|
173
|
+
if (/^\s*(?:[-*]\s*)?Trust:\s*untrusted\s*\.?\s*$/im.test(metadataHeader)) return true;
|
|
174
|
+
if (/^\s*(?:[-*]\s*)?Status:\s*(?:expired|superseded|revoked|retired)\s*\.?\s*$/im.test(metadataHeader)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const validUntil = metadataHeader.match(/^\s*(?:[-*]\s*)?Valid until:?\s+(\d{4}-\d{2}-\d{2})\s*\.?\s*$/im)?.[1];
|
|
179
|
+
if (validUntil && validUntil < now.toISOString().slice(0, 10)) return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function splitLogicalMemoryEntries(content: string): { entries: string[]; timestampDelimited: boolean } {
|
|
184
|
+
const normalized = content.replace(/^\uFEFF/, "");
|
|
185
|
+
const marker = /^<!--\s*(?:last updated:\s*)?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[[^\]]+\]\s*-->$/gm;
|
|
186
|
+
const starts = [...normalized.matchAll(marker)].map((match) => match.index ?? 0);
|
|
187
|
+
if (starts.length === 0) {
|
|
188
|
+
return { entries: normalized.split(/\n\s*\n/), timestampDelimited: false };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const entries: string[] = [];
|
|
192
|
+
const preamble = normalized.slice(0, starts[0]).trim();
|
|
193
|
+
if (preamble) entries.push(preamble);
|
|
194
|
+
for (let i = 0; i < starts.length; i++) {
|
|
195
|
+
entries.push(normalized.slice(starts[i], starts[i + 1] ?? normalized.length).trim());
|
|
196
|
+
}
|
|
197
|
+
return { entries, timestampDelimited: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isUnmarkedInactiveHeader(entry: string, now: Date): boolean {
|
|
201
|
+
if (!isInactiveMemoryEntry(entry, now)) return false;
|
|
202
|
+
return entry.split("\n").every((line) => {
|
|
203
|
+
const trimmed = line.trim();
|
|
204
|
+
return (
|
|
205
|
+
!trimmed ||
|
|
206
|
+
/^#{1,6}\s+/.test(trimmed) ||
|
|
207
|
+
/^<!--.*-->$/.test(trimmed) ||
|
|
208
|
+
/^(?:[-*]\s*)?(?:Trust:|Status:|Valid until:?\s|Source:)/i.test(trimmed)
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Apply trust, lifecycle, and secret policy to complete logical write entries. */
|
|
214
|
+
export function filterMemoryForContext(content: string, now = new Date()): string {
|
|
215
|
+
const { entries, timestampDelimited } = splitLogicalMemoryEntries(content);
|
|
216
|
+
const activeEntries: string[] = [];
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const inactive = isInactiveMemoryEntry(entry, now);
|
|
219
|
+
if (!timestampDelimited && isUnmarkedInactiveHeader(entry, now)) {
|
|
220
|
+
// Without write markers there is no reliable boundary after a metadata-only
|
|
221
|
+
// header. Fail closed instead of treating the following body paragraphs as
|
|
222
|
+
// independent trusted entries.
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (!inactive) activeEntries.push(entry);
|
|
226
|
+
}
|
|
227
|
+
return redactSecrets(activeEntries.join("\n\n")).content.trim();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sanitizeSourceUri(sourceUri?: string): string | undefined {
|
|
231
|
+
if (!sourceUri?.trim()) return undefined;
|
|
232
|
+
const singleLine = [...sourceUri]
|
|
233
|
+
.map((char) => {
|
|
234
|
+
const code = char.charCodeAt(0);
|
|
235
|
+
return code <= 31 || code === 127 ? " " : char;
|
|
236
|
+
})
|
|
237
|
+
.join("")
|
|
238
|
+
.trim()
|
|
239
|
+
.slice(0, 2_048);
|
|
240
|
+
return redactSecrets(singleLine).content;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function escapeEntryMarkers(content: string): string {
|
|
244
|
+
return content.replace(
|
|
245
|
+
/^<!--\s*(?:last updated:\s*)?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[[^\]]+\]\s*-->$/gm,
|
|
246
|
+
(line) => line.replace("<!--", "<!--"),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function formatStoredEntry(
|
|
251
|
+
content: string,
|
|
252
|
+
metadata: string,
|
|
253
|
+
sourceUri?: string,
|
|
254
|
+
): { entry: string; redacted: boolean } {
|
|
255
|
+
const safeContent = redactSecrets(escapeEntryMarkers(content));
|
|
256
|
+
const source = sanitizeSourceUri(sourceUri);
|
|
257
|
+
return {
|
|
258
|
+
entry: `${metadata}\n${safeContent.content}${source ? `\nSource: ${source}` : ""}`,
|
|
259
|
+
redacted: safeContent.redacted || (!!sourceUri && source !== sourceUri.trim()),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
155
263
|
export function truncateLines(lines: string[], maxLines: number, mode: TruncateMode) {
|
|
156
264
|
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
157
265
|
return { lines, truncated: false };
|
|
@@ -328,7 +436,7 @@ export function buildMemoryContext(searchResults?: string): string {
|
|
|
328
436
|
if (scratchpad?.trim()) {
|
|
329
437
|
const openItems = parseScratchpad(scratchpad).filter((i) => !i.done);
|
|
330
438
|
if (openItems.length > 0) {
|
|
331
|
-
const serialized = serializeScratchpad(openItems);
|
|
439
|
+
const serialized = filterMemoryForContext(serializeScratchpad(openItems));
|
|
332
440
|
const section = formatContextSection(
|
|
333
441
|
"## SCRATCHPAD.md (working context)",
|
|
334
442
|
serialized,
|
|
@@ -347,21 +455,23 @@ export function buildMemoryContext(searchResults?: string): string {
|
|
|
347
455
|
const yesterday = yesterdayStr();
|
|
348
456
|
|
|
349
457
|
const todayContent = readFileSafe(dailyPath(today));
|
|
350
|
-
|
|
458
|
+
const safeTodayContent = todayContent ? filterMemoryForContext(todayContent) : "";
|
|
459
|
+
if (safeTodayContent) {
|
|
351
460
|
const section = formatContextSection(
|
|
352
461
|
`## Daily log: ${today} (today)`,
|
|
353
|
-
|
|
354
|
-
"
|
|
462
|
+
safeTodayContent,
|
|
463
|
+
"middle",
|
|
355
464
|
CONTEXT_DAILY_MAX_LINES,
|
|
356
465
|
CONTEXT_DAILY_MAX_CHARS,
|
|
357
466
|
);
|
|
358
467
|
if (section) sections.push(section);
|
|
359
468
|
}
|
|
360
469
|
|
|
361
|
-
|
|
470
|
+
const safeSearchResults = searchResults ? filterMemoryForContext(searchResults) : "";
|
|
471
|
+
if (safeSearchResults) {
|
|
362
472
|
const section = formatContextSection(
|
|
363
473
|
"## Relevant memories (auto-retrieved)",
|
|
364
|
-
|
|
474
|
+
safeSearchResults,
|
|
365
475
|
"start",
|
|
366
476
|
CONTEXT_SEARCH_MAX_LINES,
|
|
367
477
|
CONTEXT_SEARCH_MAX_CHARS,
|
|
@@ -370,10 +480,11 @@ export function buildMemoryContext(searchResults?: string): string {
|
|
|
370
480
|
}
|
|
371
481
|
|
|
372
482
|
const longTerm = readFileSafe(MEMORY_FILE);
|
|
373
|
-
|
|
483
|
+
const safeLongTerm = longTerm ? filterMemoryForContext(longTerm) : "";
|
|
484
|
+
if (safeLongTerm) {
|
|
374
485
|
const section = formatContextSection(
|
|
375
486
|
"## MEMORY.md (long-term)",
|
|
376
|
-
|
|
487
|
+
safeLongTerm,
|
|
377
488
|
"middle",
|
|
378
489
|
CONTEXT_LONG_TERM_MAX_LINES,
|
|
379
490
|
CONTEXT_LONG_TERM_MAX_CHARS,
|
|
@@ -382,10 +493,11 @@ export function buildMemoryContext(searchResults?: string): string {
|
|
|
382
493
|
}
|
|
383
494
|
|
|
384
495
|
const yesterdayContent = readFileSafe(dailyPath(yesterday));
|
|
385
|
-
|
|
496
|
+
const safeYesterdayContent = yesterdayContent ? filterMemoryForContext(yesterdayContent) : "";
|
|
497
|
+
if (safeYesterdayContent) {
|
|
386
498
|
const section = formatContextSection(
|
|
387
499
|
`## Daily log: ${yesterday} (yesterday)`,
|
|
388
|
-
|
|
500
|
+
safeYesterdayContent,
|
|
389
501
|
"end",
|
|
390
502
|
CONTEXT_DAILY_MAX_LINES,
|
|
391
503
|
CONTEXT_DAILY_MAX_CHARS,
|
|
@@ -399,15 +511,8 @@ export function buildMemoryContext(searchResults?: string): string {
|
|
|
399
511
|
|
|
400
512
|
const context = `# Memory\n\n${sections.join("\n\n---\n\n")}`;
|
|
401
513
|
if (context.length > CONTEXT_MAX_CHARS) {
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
maxChars: CONTEXT_MAX_CHARS,
|
|
405
|
-
mode: "start",
|
|
406
|
-
});
|
|
407
|
-
const note = result.truncated
|
|
408
|
-
? `\n\n[truncated overall context: showing ${result.previewChars}/${result.totalChars} chars]`
|
|
409
|
-
: "";
|
|
410
|
-
return `${result.preview}${note}`;
|
|
514
|
+
const note = "\n\n[truncated overall context to 16000 chars]";
|
|
515
|
+
return context.slice(0, CONTEXT_MAX_CHARS - note.length).trimEnd() + note;
|
|
411
516
|
}
|
|
412
517
|
|
|
413
518
|
return context;
|
|
@@ -430,10 +535,11 @@ function buildTopicsContextSection(): string | null {
|
|
|
430
535
|
for (const file of topicFiles) {
|
|
431
536
|
const slug = file.replace(/\.md$/, "");
|
|
432
537
|
const content = readFileSafe(path.join(TOPICS_DIR, file));
|
|
433
|
-
|
|
434
|
-
|
|
538
|
+
const safeContent = content ? filterMemoryForContext(content) : "";
|
|
539
|
+
if (!safeContent) continue;
|
|
540
|
+
const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
|
|
435
541
|
const title = titleMatch?.[1]?.trim() || slug;
|
|
436
|
-
entries.push(...parseTopicEntries(title, slug,
|
|
542
|
+
entries.push(...parseTopicEntries(title, slug, safeContent));
|
|
437
543
|
}
|
|
438
544
|
|
|
439
545
|
if (entries.length === 0) return null;
|
|
@@ -1056,9 +1162,61 @@ export async function getQmdHealth(): Promise<QmdHealthInfo | null> {
|
|
|
1056
1162
|
});
|
|
1057
1163
|
}
|
|
1058
1164
|
|
|
1165
|
+
function resolveCaseInsensitivePath(root: string, relativePath: string): string | null {
|
|
1166
|
+
let current = root;
|
|
1167
|
+
for (const segment of relativePath.split(/[/\\]+/).filter(Boolean)) {
|
|
1168
|
+
if (segment === "." || segment === "..") return null;
|
|
1169
|
+
let children: string[];
|
|
1170
|
+
try {
|
|
1171
|
+
children = fs.readdirSync(current);
|
|
1172
|
+
} catch {
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
const child =
|
|
1176
|
+
children.find((name) => name === segment) ??
|
|
1177
|
+
children.find((name) => name.toLowerCase() === segment.toLowerCase());
|
|
1178
|
+
if (!child) return null;
|
|
1179
|
+
current = path.join(current, child);
|
|
1180
|
+
}
|
|
1181
|
+
return current;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function resolveQmdSourcePath(filePath: string): string | null {
|
|
1185
|
+
const qmdUri = filePath.match(/^qmd:\/\/([^/]+)\/?(.*)$/i);
|
|
1186
|
+
let relativePath = qmdUri ? qmdUri[2] : filePath;
|
|
1187
|
+
if (relativePath.toLowerCase().startsWith(`${QMD_COLLECTION_NAME.toLowerCase()}/`)) {
|
|
1188
|
+
relativePath = relativePath.slice(QMD_COLLECTION_NAME.length + 1);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const root = path.resolve(MEMORY_DIR);
|
|
1192
|
+
if (path.isAbsolute(relativePath)) {
|
|
1193
|
+
const candidate = path.resolve(relativePath);
|
|
1194
|
+
if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`)) return null;
|
|
1195
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
1196
|
+
}
|
|
1197
|
+
return resolveCaseInsensitivePath(root, relativePath.replace(/^\/+/, ""));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function qmdResultPassesSourcePolicy(filePath: string | undefined, snippet: string): boolean {
|
|
1201
|
+
if (!filePath) return false;
|
|
1202
|
+
const sourcePath = resolveQmdSourcePath(filePath);
|
|
1203
|
+
if (!sourcePath) return false;
|
|
1204
|
+
const source = readFileSafe(sourcePath);
|
|
1205
|
+
if (!source) return false;
|
|
1206
|
+
|
|
1207
|
+
const activeSource = filterMemoryForContext(source);
|
|
1208
|
+
const snippetLines = snippet
|
|
1209
|
+
.split("\n")
|
|
1210
|
+
.map((line) => line.trim())
|
|
1211
|
+
.filter((line) => line.length >= 8);
|
|
1212
|
+
return snippetLines.length > 0 && snippetLines.every((line) => activeSource.includes(line));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1059
1215
|
/** Search for memories relevant to the user's prompt. Returns formatted markdown or empty string on error. */
|
|
1060
1216
|
export async function searchRelevantMemories(prompt: string): Promise<string> {
|
|
1061
1217
|
if (!qmdAvailable || !prompt.trim()) return "";
|
|
1218
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1219
|
+
const controller = new AbortController();
|
|
1062
1220
|
|
|
1063
1221
|
// Sanitize: strip control chars, limit to 200 chars for the search query
|
|
1064
1222
|
const sanitized = prompt
|
|
@@ -1073,19 +1231,25 @@ export async function searchRelevantMemories(prompt: string): Promise<string> {
|
|
|
1073
1231
|
if (!hasCollection) return "";
|
|
1074
1232
|
|
|
1075
1233
|
const results = await Promise.race([
|
|
1076
|
-
runQmdSearch("keyword", sanitized, 3),
|
|
1077
|
-
new Promise<never>((_, reject) =>
|
|
1234
|
+
runQmdSearch("keyword", sanitized, 3, { signal: controller.signal }),
|
|
1235
|
+
new Promise<never>((_, reject) => {
|
|
1236
|
+
timer = setTimeout(() => {
|
|
1237
|
+
controller.abort();
|
|
1238
|
+
reject(new Error("timeout"));
|
|
1239
|
+
}, 3_000);
|
|
1240
|
+
}),
|
|
1078
1241
|
]);
|
|
1079
1242
|
|
|
1080
1243
|
if (!results || results.results.length === 0) return "";
|
|
1081
1244
|
|
|
1082
1245
|
const snippets = results.results
|
|
1083
1246
|
.map((r) => {
|
|
1084
|
-
const text = getQmdResultText(r);
|
|
1085
|
-
if (!text
|
|
1247
|
+
const text = filterMemoryForContext(getQmdResultText(r));
|
|
1248
|
+
if (!text) return null;
|
|
1086
1249
|
const filePath = getQmdResultPath(r);
|
|
1250
|
+
if (!qmdResultPassesSourcePolicy(filePath, text)) return null;
|
|
1087
1251
|
const filePart = filePath ? `_${filePath}_` : "";
|
|
1088
|
-
return filePart ? `${filePart}\n${text
|
|
1252
|
+
return filePart ? `${filePart}\n${text}` : text;
|
|
1089
1253
|
})
|
|
1090
1254
|
.filter(Boolean);
|
|
1091
1255
|
|
|
@@ -1093,12 +1257,15 @@ export async function searchRelevantMemories(prompt: string): Promise<string> {
|
|
|
1093
1257
|
return snippets.join("\n\n---\n\n");
|
|
1094
1258
|
} catch {
|
|
1095
1259
|
return "";
|
|
1260
|
+
} finally {
|
|
1261
|
+
clearTimeout(timer);
|
|
1096
1262
|
}
|
|
1097
1263
|
}
|
|
1098
1264
|
|
|
1099
1265
|
export interface QmdSearchResult {
|
|
1100
1266
|
path?: string;
|
|
1101
1267
|
file?: string;
|
|
1268
|
+
context?: string;
|
|
1102
1269
|
score?: number;
|
|
1103
1270
|
content?: string;
|
|
1104
1271
|
chunk?: string;
|
|
@@ -1112,7 +1279,20 @@ export function getQmdResultPath(r: QmdSearchResult): string | undefined {
|
|
|
1112
1279
|
}
|
|
1113
1280
|
|
|
1114
1281
|
export function getQmdResultText(r: QmdSearchResult): string {
|
|
1115
|
-
|
|
1282
|
+
const text = r.content ?? r.chunk ?? r.snippet ?? "";
|
|
1283
|
+
const folderContext = r.context?.trim();
|
|
1284
|
+
let normalized = text.trimStart();
|
|
1285
|
+
if (folderContext) {
|
|
1286
|
+
const prefix = `Folder Context: ${folderContext}`;
|
|
1287
|
+
if (normalized.startsWith(prefix)) {
|
|
1288
|
+
const remainder = normalized.slice(prefix.length);
|
|
1289
|
+
if (/^(?:\r?\n){2}/.test(remainder)) {
|
|
1290
|
+
normalized = remainder.replace(/^(?:\r?\n){2}/, "");
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
return normalized.replace(/^@@ -\d+(?:,\d+)?(?: \+\d+(?:,\d+)?)? @@ \(\d+ before, \d+ after\)(?:\r?\n){1,2}/, "");
|
|
1116
1296
|
}
|
|
1117
1297
|
|
|
1118
1298
|
function stripAnsi(text: string): string {
|
|
@@ -1146,12 +1326,13 @@ export function runQmdSearch(
|
|
|
1146
1326
|
mode: "keyword" | "semantic" | "deep",
|
|
1147
1327
|
query: string,
|
|
1148
1328
|
limit: number,
|
|
1329
|
+
options: { signal?: AbortSignal } = {},
|
|
1149
1330
|
): Promise<{ results: QmdSearchResult[]; stderr: string }> {
|
|
1150
1331
|
const subcommand = mode === "keyword" ? "search" : mode === "semantic" ? "vsearch" : "query";
|
|
1151
1332
|
const args = [subcommand, "--json", "-c", QMD_COLLECTION_NAME, "-n", String(limit), query];
|
|
1152
1333
|
|
|
1153
1334
|
return new Promise((resolve, reject) => {
|
|
1154
|
-
execFileFn("qmd", args, { timeout: 60_000 }, (err, stdout, stderr) => {
|
|
1335
|
+
execFileFn("qmd", args, { timeout: 60_000, signal: options.signal }, (err, stdout, stderr) => {
|
|
1155
1336
|
if (err) {
|
|
1156
1337
|
reject(new Error(stderr?.trim() || err.message));
|
|
1157
1338
|
return;
|
|
@@ -1171,6 +1352,39 @@ export function runQmdSearch(
|
|
|
1171
1352
|
});
|
|
1172
1353
|
}
|
|
1173
1354
|
|
|
1355
|
+
/**
|
|
1356
|
+
* Best-effort check of whether vector embeddings are actually usable for
|
|
1357
|
+
* semantic/deep search right now. Runs a tiny semantic probe and looks for
|
|
1358
|
+
* qmd's "need embeddings" warning. Bounded by a short timeout because the very
|
|
1359
|
+
* first semantic query can trigger a model download — returns "unknown" rather
|
|
1360
|
+
* than blocking on it. "ready" means the probe ran without the warning; it does
|
|
1361
|
+
* not prove the index has content.
|
|
1362
|
+
*/
|
|
1363
|
+
export async function probeEmbeddings(): Promise<"ready" | "missing" | "unknown"> {
|
|
1364
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1365
|
+
// Abort the underlying qmd child when the timeout fires so it does not keep
|
|
1366
|
+
// the event loop open until its own 60s timeout and hang the CLI.
|
|
1367
|
+
const controller = new AbortController();
|
|
1368
|
+
try {
|
|
1369
|
+
const { stderr } = await Promise.race([
|
|
1370
|
+
runQmdSearch("semantic", "memory", 1, { signal: controller.signal }),
|
|
1371
|
+
new Promise<never>((_, reject) => {
|
|
1372
|
+
timer = setTimeout(() => {
|
|
1373
|
+
controller.abort();
|
|
1374
|
+
reject(new Error("timeout"));
|
|
1375
|
+
}, 4_000);
|
|
1376
|
+
}),
|
|
1377
|
+
]);
|
|
1378
|
+
return /need embeddings/i.test(stderr ?? "") ? "missing" : "ready";
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1381
|
+
if (/need embeddings/i.test(msg)) return "missing";
|
|
1382
|
+
return "unknown";
|
|
1383
|
+
} finally {
|
|
1384
|
+
clearTimeout(timer);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1174
1388
|
// ---------------------------------------------------------------------------
|
|
1175
1389
|
// Standalone tool functions
|
|
1176
1390
|
// ---------------------------------------------------------------------------
|
|
@@ -1188,6 +1402,7 @@ export async function memoryWrite(params: {
|
|
|
1188
1402
|
sessionId?: string;
|
|
1189
1403
|
topic?: string;
|
|
1190
1404
|
date?: string;
|
|
1405
|
+
sourceUri?: string;
|
|
1191
1406
|
}): Promise<ToolResult> {
|
|
1192
1407
|
ensureDirs();
|
|
1193
1408
|
const target = params.target ?? "daily";
|
|
@@ -1198,18 +1413,19 @@ export async function memoryWrite(params: {
|
|
|
1198
1413
|
if (target === "daily") {
|
|
1199
1414
|
const filePath = dailyPath(todayStr());
|
|
1200
1415
|
const existing = readFileSafe(filePath) ?? "";
|
|
1201
|
-
const
|
|
1416
|
+
const safeExisting = redactSecrets(existing).content;
|
|
1417
|
+
const existingPreview = buildPreview(safeExisting, {
|
|
1202
1418
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
1203
1419
|
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
1204
1420
|
mode: "end",
|
|
1205
1421
|
});
|
|
1206
1422
|
const existingSnippet = existingPreview.preview
|
|
1207
|
-
? `\n\n${formatPreviewBlock("Existing daily log preview",
|
|
1423
|
+
? `\n\n${formatPreviewBlock("Existing daily log preview", safeExisting, "end")}`
|
|
1208
1424
|
: "\n\nDaily log was empty.";
|
|
1209
1425
|
|
|
1210
1426
|
const separator = existing.trim() ? "\n\n" : "";
|
|
1211
|
-
const
|
|
1212
|
-
fs.writeFileSync(filePath, existing + separator +
|
|
1427
|
+
const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
|
|
1428
|
+
fs.writeFileSync(filePath, existing + separator + stored.entry, "utf-8");
|
|
1213
1429
|
await ensureQmdAvailableForUpdate();
|
|
1214
1430
|
scheduleQmdUpdate();
|
|
1215
1431
|
return {
|
|
@@ -1220,6 +1436,8 @@ export async function memoryWrite(params: {
|
|
|
1220
1436
|
mode: "append",
|
|
1221
1437
|
sessionId: sid,
|
|
1222
1438
|
timestamp: ts,
|
|
1439
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1440
|
+
redacted: stored.redacted,
|
|
1223
1441
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1224
1442
|
existingPreview,
|
|
1225
1443
|
},
|
|
@@ -1237,21 +1455,26 @@ export async function memoryWrite(params: {
|
|
|
1237
1455
|
}
|
|
1238
1456
|
const filePath = topicPath(slug);
|
|
1239
1457
|
const existing = readFileSafe(filePath) ?? "";
|
|
1240
|
-
const
|
|
1458
|
+
const safeExisting = redactSecrets(existing).content;
|
|
1459
|
+
const existingPreview = buildPreview(safeExisting, {
|
|
1241
1460
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
1242
1461
|
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
1243
1462
|
mode: "end",
|
|
1244
1463
|
});
|
|
1245
1464
|
const existingSnippet = existingPreview.preview
|
|
1246
|
-
? `\n\n${formatPreviewBlock("Existing topic preview",
|
|
1465
|
+
? `\n\n${formatPreviewBlock("Existing topic preview", safeExisting, "end")}`
|
|
1247
1466
|
: "\n\nTopic file was empty.";
|
|
1248
1467
|
|
|
1249
1468
|
const linkDate = params.date?.trim() || todayStr();
|
|
1250
1469
|
const header = `# Topic: ${topic}\n\n<!-- created: ${ts} [${sid}] -->\n`;
|
|
1251
1470
|
const separator = existing.trim() ? "\n\n" : "";
|
|
1252
1471
|
const base = existing.trim() ? existing : header.trimEnd();
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1472
|
+
const stored = formatStoredEntry(
|
|
1473
|
+
`${content.trim()}\nDaily: [[${linkDate}]]`,
|
|
1474
|
+
`<!-- ${ts} [${sid}] -->`,
|
|
1475
|
+
params.sourceUri,
|
|
1476
|
+
);
|
|
1477
|
+
fs.writeFileSync(filePath, `${base}${separator}${stored.entry}`, "utf-8");
|
|
1255
1478
|
await ensureQmdAvailableForUpdate();
|
|
1256
1479
|
scheduleQmdUpdate();
|
|
1257
1480
|
return {
|
|
@@ -1265,6 +1488,8 @@ export async function memoryWrite(params: {
|
|
|
1265
1488
|
topic,
|
|
1266
1489
|
slug,
|
|
1267
1490
|
date: linkDate,
|
|
1491
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1492
|
+
redacted: stored.redacted,
|
|
1268
1493
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1269
1494
|
existingPreview,
|
|
1270
1495
|
},
|
|
@@ -1274,18 +1499,19 @@ export async function memoryWrite(params: {
|
|
|
1274
1499
|
// long_term
|
|
1275
1500
|
const memFile = getMemoryFile();
|
|
1276
1501
|
const existing = readFileSafe(memFile) ?? "";
|
|
1277
|
-
const
|
|
1502
|
+
const safeExisting = redactSecrets(existing).content;
|
|
1503
|
+
const existingPreview = buildPreview(safeExisting, {
|
|
1278
1504
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
1279
1505
|
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
1280
1506
|
mode: "middle",
|
|
1281
1507
|
});
|
|
1282
1508
|
const existingSnippet = existingPreview.preview
|
|
1283
|
-
? `\n\n${formatPreviewBlock("Existing MEMORY.md preview",
|
|
1509
|
+
? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", safeExisting, "middle")}`
|
|
1284
1510
|
: "\n\nMEMORY.md was empty.";
|
|
1285
1511
|
|
|
1286
1512
|
if (mode === "overwrite") {
|
|
1287
|
-
const
|
|
1288
|
-
fs.writeFileSync(memFile,
|
|
1513
|
+
const stored = formatStoredEntry(content, `<!-- last updated: ${ts} [${sid}] -->`, params.sourceUri);
|
|
1514
|
+
fs.writeFileSync(memFile, stored.entry, "utf-8");
|
|
1289
1515
|
await ensureQmdAvailableForUpdate();
|
|
1290
1516
|
scheduleQmdUpdate();
|
|
1291
1517
|
return {
|
|
@@ -1296,6 +1522,8 @@ export async function memoryWrite(params: {
|
|
|
1296
1522
|
mode: "overwrite",
|
|
1297
1523
|
sessionId: sid,
|
|
1298
1524
|
timestamp: ts,
|
|
1525
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1526
|
+
redacted: stored.redacted,
|
|
1299
1527
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1300
1528
|
existingPreview,
|
|
1301
1529
|
},
|
|
@@ -1304,8 +1532,8 @@ export async function memoryWrite(params: {
|
|
|
1304
1532
|
|
|
1305
1533
|
// append (default)
|
|
1306
1534
|
const separator = existing.trim() ? "\n\n" : "";
|
|
1307
|
-
const
|
|
1308
|
-
fs.writeFileSync(memFile, existing + separator +
|
|
1535
|
+
const stored = formatStoredEntry(content, `<!-- ${ts} [${sid}] -->`, params.sourceUri);
|
|
1536
|
+
fs.writeFileSync(memFile, existing + separator + stored.entry, "utf-8");
|
|
1309
1537
|
await ensureQmdAvailableForUpdate();
|
|
1310
1538
|
scheduleQmdUpdate();
|
|
1311
1539
|
return {
|
|
@@ -1316,6 +1544,8 @@ export async function memoryWrite(params: {
|
|
|
1316
1544
|
mode: "append",
|
|
1317
1545
|
sessionId: sid,
|
|
1318
1546
|
timestamp: ts,
|
|
1547
|
+
sourceUri: sanitizeSourceUri(params.sourceUri),
|
|
1548
|
+
redacted: stored.redacted,
|
|
1319
1549
|
qmdUpdateMode: getQmdUpdateMode(),
|
|
1320
1550
|
existingPreview,
|
|
1321
1551
|
},
|
|
@@ -1334,7 +1564,11 @@ export async function scratchpadAction(params: {
|
|
|
1334
1564
|
const spFile = getScratchpadFile();
|
|
1335
1565
|
|
|
1336
1566
|
const existing = readFileSafe(spFile) ?? "";
|
|
1337
|
-
let items = parseScratchpad(existing)
|
|
1567
|
+
let items = parseScratchpad(existing).map((item) => ({
|
|
1568
|
+
...item,
|
|
1569
|
+
text: redactSecrets(item.text).content,
|
|
1570
|
+
meta: redactSecrets(item.meta).content,
|
|
1571
|
+
}));
|
|
1338
1572
|
|
|
1339
1573
|
if (action === "list") {
|
|
1340
1574
|
if (items.length === 0) {
|
|
@@ -1360,7 +1594,8 @@ export async function scratchpadAction(params: {
|
|
|
1360
1594
|
if (!text) {
|
|
1361
1595
|
return { text: "Error: 'text' is required for add.", details: {} };
|
|
1362
1596
|
}
|
|
1363
|
-
|
|
1597
|
+
const safeText = redactSecrets(text).content;
|
|
1598
|
+
items.push({ done: false, text: safeText, meta: `<!-- ${ts} [${sid}] -->` });
|
|
1364
1599
|
const serialized = serializeScratchpad(items);
|
|
1365
1600
|
const preview = buildPreview(serialized, {
|
|
1366
1601
|
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
@@ -1371,7 +1606,7 @@ export async function scratchpadAction(params: {
|
|
|
1371
1606
|
await ensureQmdAvailableForUpdate();
|
|
1372
1607
|
scheduleQmdUpdate();
|
|
1373
1608
|
return {
|
|
1374
|
-
text: `Added: - [ ] ${
|
|
1609
|
+
text: `Added: - [ ] ${safeText}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
|
|
1375
1610
|
details: {
|
|
1376
1611
|
action,
|
|
1377
1612
|
sessionId: sid,
|
|
@@ -1871,7 +2106,9 @@ export async function distilMemories(params?: { dryRun?: boolean; sessionId?: st
|
|
|
1871
2106
|
const date = file.replace(/\.md$/, "");
|
|
1872
2107
|
const content = readFileSafe(path.join(DAILY_DIR, file));
|
|
1873
2108
|
if (!content?.trim()) continue;
|
|
1874
|
-
|
|
2109
|
+
const safeContent = filterMemoryForContext(content);
|
|
2110
|
+
if (!safeContent) continue;
|
|
2111
|
+
allEntries.push(...parseDailyEntries(date, safeContent));
|
|
1875
2112
|
}
|
|
1876
2113
|
const topicEntriesByTopic = new Map<string, TopicEntry[]>();
|
|
1877
2114
|
let totalTopicEntries = 0;
|
|
@@ -1879,9 +2116,11 @@ export async function distilMemories(params?: { dryRun?: boolean; sessionId?: st
|
|
|
1879
2116
|
const slug = file.replace(/\.md$/, "");
|
|
1880
2117
|
const content = readFileSafe(path.join(TOPICS_DIR, file));
|
|
1881
2118
|
if (!content?.trim()) continue;
|
|
1882
|
-
const
|
|
2119
|
+
const safeContent = filterMemoryForContext(content);
|
|
2120
|
+
if (!safeContent) continue;
|
|
2121
|
+
const titleMatch = safeContent.match(/^# Topic:\s*(.+)$/m);
|
|
1883
2122
|
const title = titleMatch?.[1]?.trim() || slug;
|
|
1884
|
-
const entries = parseTopicEntries(title, slug,
|
|
2123
|
+
const entries = parseTopicEntries(title, slug, safeContent);
|
|
1885
2124
|
if (entries.length === 0) continue;
|
|
1886
2125
|
totalTopicEntries += entries.length;
|
|
1887
2126
|
allEntries.push(...entries);
|
|
@@ -1929,7 +2168,8 @@ export async function distilMemories(params?: { dryRun?: boolean; sessionId?: st
|
|
|
1929
2168
|
let pinnedSection = "";
|
|
1930
2169
|
const existingMemory = readFileSafe(MEMORY_FILE);
|
|
1931
2170
|
if (existingMemory) {
|
|
1932
|
-
const
|
|
2171
|
+
const safeExistingMemory = filterMemoryForContext(existingMemory);
|
|
2172
|
+
const pinnedMatch = safeExistingMemory.match(/## Pinned\n([\s\S]*?)(?=\n## |\n# |$)/);
|
|
1933
2173
|
if (pinnedMatch) {
|
|
1934
2174
|
pinnedSection = pinnedMatch[1].trim();
|
|
1935
2175
|
}
|
package/dist/agent-memory
DELETED
|
Binary file
|