myagentmemory 0.4.3
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/LICENSE +21 -0
- package/README.md +231 -0
- package/dist/agent-memory +0 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +510 -0
- package/dist/core.d.ts +131 -0
- package/dist/core.js +883 -0
- package/package.json +70 -0
- package/scripts/install-skills.ps1 +48 -0
- package/scripts/install-skills.sh +55 -0
- package/scripts/postinstall.cjs +44 -0
- package/skills/claude-code/SKILL.md +101 -0
- package/skills/codex/SKILL.md +104 -0
- package/src/cli.ts +596 -0
- package/src/core.ts +1082 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared core logic for agent-memory.
|
|
3
|
+
*
|
|
4
|
+
* Core logic for agent-memory CLI and skills.
|
|
5
|
+
* Zero pi peer dependencies — only node:fs, node:path, node:child_process.
|
|
6
|
+
*/
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Paths (mutable for testing via _setBaseDir / _resetBaseDir)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const DEFAULT_MEMORY_DIR = process.env.AGENT_MEMORY_DIR ?? path.join(process.env.HOME ?? "~", ".agent-memory");
|
|
14
|
+
let MEMORY_DIR = DEFAULT_MEMORY_DIR;
|
|
15
|
+
let MEMORY_FILE = path.join(MEMORY_DIR, "MEMORY.md");
|
|
16
|
+
let SCRATCHPAD_FILE = path.join(MEMORY_DIR, "SCRATCHPAD.md");
|
|
17
|
+
let DAILY_DIR = path.join(MEMORY_DIR, "daily");
|
|
18
|
+
/** Override base directory (for testing or platform-specific defaults). */
|
|
19
|
+
export function _setBaseDir(baseDir) {
|
|
20
|
+
MEMORY_DIR = baseDir;
|
|
21
|
+
MEMORY_FILE = path.join(baseDir, "MEMORY.md");
|
|
22
|
+
SCRATCHPAD_FILE = path.join(baseDir, "SCRATCHPAD.md");
|
|
23
|
+
DAILY_DIR = path.join(baseDir, "daily");
|
|
24
|
+
}
|
|
25
|
+
/** Reset to default paths. */
|
|
26
|
+
export function _resetBaseDir() {
|
|
27
|
+
_setBaseDir(DEFAULT_MEMORY_DIR);
|
|
28
|
+
}
|
|
29
|
+
/** Get the current memory directory path. */
|
|
30
|
+
export function getMemoryDir() {
|
|
31
|
+
return MEMORY_DIR;
|
|
32
|
+
}
|
|
33
|
+
/** Get the current MEMORY.md path. */
|
|
34
|
+
export function getMemoryFile() {
|
|
35
|
+
return MEMORY_FILE;
|
|
36
|
+
}
|
|
37
|
+
/** Get the current SCRATCHPAD.md path. */
|
|
38
|
+
export function getScratchpadFile() {
|
|
39
|
+
return SCRATCHPAD_FILE;
|
|
40
|
+
}
|
|
41
|
+
/** Get the current daily log directory path. */
|
|
42
|
+
export function getDailyDir() {
|
|
43
|
+
return DAILY_DIR;
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Utilities
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
export function ensureDirs() {
|
|
49
|
+
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
50
|
+
fs.mkdirSync(DAILY_DIR, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
export function todayStr() {
|
|
53
|
+
const d = new Date();
|
|
54
|
+
return d.toISOString().slice(0, 10);
|
|
55
|
+
}
|
|
56
|
+
export function yesterdayStr() {
|
|
57
|
+
const d = new Date();
|
|
58
|
+
d.setDate(d.getDate() - 1);
|
|
59
|
+
return d.toISOString().slice(0, 10);
|
|
60
|
+
}
|
|
61
|
+
export function nowTimestamp() {
|
|
62
|
+
return new Date()
|
|
63
|
+
.toISOString()
|
|
64
|
+
.replace("T", " ")
|
|
65
|
+
.replace(/\.\d+Z$/, "");
|
|
66
|
+
}
|
|
67
|
+
export function shortSessionId(sessionId) {
|
|
68
|
+
return sessionId.slice(0, 8);
|
|
69
|
+
}
|
|
70
|
+
export function readFileSafe(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export function dailyPath(date) {
|
|
79
|
+
return path.join(DAILY_DIR, `${date}.md`);
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Limits + preview helpers
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export const RESPONSE_PREVIEW_MAX_CHARS = 4_000;
|
|
85
|
+
export const RESPONSE_PREVIEW_MAX_LINES = 120;
|
|
86
|
+
const CONTEXT_LONG_TERM_MAX_CHARS = 4_000;
|
|
87
|
+
const CONTEXT_LONG_TERM_MAX_LINES = 150;
|
|
88
|
+
const CONTEXT_SCRATCHPAD_MAX_CHARS = 2_000;
|
|
89
|
+
const CONTEXT_SCRATCHPAD_MAX_LINES = 120;
|
|
90
|
+
const CONTEXT_DAILY_MAX_CHARS = 3_000;
|
|
91
|
+
const CONTEXT_DAILY_MAX_LINES = 120;
|
|
92
|
+
const CONTEXT_SEARCH_MAX_CHARS = 2_500;
|
|
93
|
+
const CONTEXT_SEARCH_MAX_LINES = 80;
|
|
94
|
+
const CONTEXT_MAX_CHARS = 16_000;
|
|
95
|
+
function normalizeContent(content) {
|
|
96
|
+
return content.trim();
|
|
97
|
+
}
|
|
98
|
+
export function truncateLines(lines, maxLines, mode) {
|
|
99
|
+
if (maxLines <= 0 || lines.length <= maxLines) {
|
|
100
|
+
return { lines, truncated: false };
|
|
101
|
+
}
|
|
102
|
+
if (mode === "end") {
|
|
103
|
+
return { lines: lines.slice(-maxLines), truncated: true };
|
|
104
|
+
}
|
|
105
|
+
if (mode === "middle" && maxLines > 1) {
|
|
106
|
+
const marker = "... (truncated) ...";
|
|
107
|
+
const keep = maxLines - 1;
|
|
108
|
+
const headCount = Math.ceil(keep / 2);
|
|
109
|
+
const tailCount = Math.floor(keep / 2);
|
|
110
|
+
const head = lines.slice(0, headCount);
|
|
111
|
+
const tail = tailCount > 0 ? lines.slice(-tailCount) : [];
|
|
112
|
+
return { lines: [...head, marker, ...tail], truncated: true };
|
|
113
|
+
}
|
|
114
|
+
return { lines: lines.slice(0, maxLines), truncated: true };
|
|
115
|
+
}
|
|
116
|
+
export function truncateText(text, maxChars, mode) {
|
|
117
|
+
if (maxChars <= 0 || text.length <= maxChars) {
|
|
118
|
+
return { text, truncated: false };
|
|
119
|
+
}
|
|
120
|
+
if (mode === "end") {
|
|
121
|
+
return { text: text.slice(-maxChars), truncated: true };
|
|
122
|
+
}
|
|
123
|
+
if (mode === "middle" && maxChars > 10) {
|
|
124
|
+
const marker = "... (truncated) ...";
|
|
125
|
+
const keep = maxChars - marker.length;
|
|
126
|
+
if (keep > 0) {
|
|
127
|
+
const headCount = Math.ceil(keep / 2);
|
|
128
|
+
const tailCount = Math.floor(keep / 2);
|
|
129
|
+
return {
|
|
130
|
+
text: text.slice(0, headCount) + marker + text.slice(text.length - tailCount),
|
|
131
|
+
truncated: true,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { text: text.slice(0, maxChars), truncated: true };
|
|
136
|
+
}
|
|
137
|
+
export function buildPreview(content, options) {
|
|
138
|
+
const normalized = normalizeContent(content);
|
|
139
|
+
if (!normalized) {
|
|
140
|
+
return {
|
|
141
|
+
preview: "",
|
|
142
|
+
truncated: false,
|
|
143
|
+
totalLines: 0,
|
|
144
|
+
totalChars: 0,
|
|
145
|
+
previewLines: 0,
|
|
146
|
+
previewChars: 0,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const lines = normalized.split("\n");
|
|
150
|
+
const totalLines = lines.length;
|
|
151
|
+
const totalChars = normalized.length;
|
|
152
|
+
const lineResult = truncateLines(lines, options.maxLines, options.mode);
|
|
153
|
+
const text = lineResult.lines.join("\n");
|
|
154
|
+
const charResult = truncateText(text, options.maxChars, options.mode);
|
|
155
|
+
const preview = charResult.text;
|
|
156
|
+
const previewLines = preview ? preview.split("\n").length : 0;
|
|
157
|
+
const previewChars = preview.length;
|
|
158
|
+
return {
|
|
159
|
+
preview,
|
|
160
|
+
truncated: lineResult.truncated || charResult.truncated,
|
|
161
|
+
totalLines,
|
|
162
|
+
totalChars,
|
|
163
|
+
previewLines,
|
|
164
|
+
previewChars,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function formatPreviewBlock(label, content, mode) {
|
|
168
|
+
const result = buildPreview(content, {
|
|
169
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
170
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
171
|
+
mode,
|
|
172
|
+
});
|
|
173
|
+
if (!result.preview) {
|
|
174
|
+
return `${label}: empty.`;
|
|
175
|
+
}
|
|
176
|
+
const meta = `${label} (${result.totalLines} lines, ${result.totalChars} chars)`;
|
|
177
|
+
const note = result.truncated
|
|
178
|
+
? `\n[preview truncated: showing ${result.previewLines}/${result.totalLines} lines, ${result.previewChars}/${result.totalChars} chars]`
|
|
179
|
+
: "";
|
|
180
|
+
return `${meta}\n\n${result.preview}${note}`;
|
|
181
|
+
}
|
|
182
|
+
export function formatContextSection(label, content, mode, maxLines, maxChars) {
|
|
183
|
+
const result = buildPreview(content, { maxLines, maxChars, mode });
|
|
184
|
+
if (!result.preview) {
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
const note = result.truncated
|
|
188
|
+
? `\n\n[truncated: showing ${result.previewLines}/${result.totalLines} lines, ${result.previewChars}/${result.totalChars} chars]`
|
|
189
|
+
: "";
|
|
190
|
+
return `${label}\n\n${result.preview}${note}`;
|
|
191
|
+
}
|
|
192
|
+
export function parseScratchpad(content) {
|
|
193
|
+
const items = [];
|
|
194
|
+
const lines = content.split("\n");
|
|
195
|
+
for (let i = 0; i < lines.length; i++) {
|
|
196
|
+
const line = lines[i];
|
|
197
|
+
const match = line.match(/^- \[([ xX])\] (.+)$/);
|
|
198
|
+
if (match) {
|
|
199
|
+
let meta = "";
|
|
200
|
+
if (i > 0 && lines[i - 1].match(/^<!--.*-->$/)) {
|
|
201
|
+
meta = lines[i - 1];
|
|
202
|
+
}
|
|
203
|
+
items.push({
|
|
204
|
+
done: match[1].toLowerCase() === "x",
|
|
205
|
+
text: match[2],
|
|
206
|
+
meta,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return items;
|
|
211
|
+
}
|
|
212
|
+
export function serializeScratchpad(items) {
|
|
213
|
+
const lines = ["# Scratchpad", ""];
|
|
214
|
+
for (const item of items) {
|
|
215
|
+
if (item.meta) {
|
|
216
|
+
lines.push(item.meta);
|
|
217
|
+
}
|
|
218
|
+
const checkbox = item.done ? "[x]" : "[ ]";
|
|
219
|
+
lines.push(`- ${checkbox} ${item.text}`);
|
|
220
|
+
}
|
|
221
|
+
return `${lines.join("\n")}\n`;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Context builder
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
export function buildMemoryContext(searchResults) {
|
|
227
|
+
ensureDirs();
|
|
228
|
+
// Priority order: scratchpad > today's daily > search results > MEMORY.md > yesterday's daily
|
|
229
|
+
const sections = [];
|
|
230
|
+
const scratchpad = readFileSafe(SCRATCHPAD_FILE);
|
|
231
|
+
if (scratchpad?.trim()) {
|
|
232
|
+
const openItems = parseScratchpad(scratchpad).filter((i) => !i.done);
|
|
233
|
+
if (openItems.length > 0) {
|
|
234
|
+
const serialized = serializeScratchpad(openItems);
|
|
235
|
+
const section = formatContextSection("## SCRATCHPAD.md (working context)", serialized, "start", CONTEXT_SCRATCHPAD_MAX_LINES, CONTEXT_SCRATCHPAD_MAX_CHARS);
|
|
236
|
+
if (section)
|
|
237
|
+
sections.push(section);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const today = todayStr();
|
|
241
|
+
const yesterday = yesterdayStr();
|
|
242
|
+
const todayContent = readFileSafe(dailyPath(today));
|
|
243
|
+
if (todayContent?.trim()) {
|
|
244
|
+
const section = formatContextSection(`## Daily log: ${today} (today)`, todayContent, "end", CONTEXT_DAILY_MAX_LINES, CONTEXT_DAILY_MAX_CHARS);
|
|
245
|
+
if (section)
|
|
246
|
+
sections.push(section);
|
|
247
|
+
}
|
|
248
|
+
if (searchResults?.trim()) {
|
|
249
|
+
const section = formatContextSection("## Relevant memories (auto-retrieved)", searchResults, "start", CONTEXT_SEARCH_MAX_LINES, CONTEXT_SEARCH_MAX_CHARS);
|
|
250
|
+
if (section)
|
|
251
|
+
sections.push(section);
|
|
252
|
+
}
|
|
253
|
+
const longTerm = readFileSafe(MEMORY_FILE);
|
|
254
|
+
if (longTerm?.trim()) {
|
|
255
|
+
const section = formatContextSection("## MEMORY.md (long-term)", longTerm, "middle", CONTEXT_LONG_TERM_MAX_LINES, CONTEXT_LONG_TERM_MAX_CHARS);
|
|
256
|
+
if (section)
|
|
257
|
+
sections.push(section);
|
|
258
|
+
}
|
|
259
|
+
const yesterdayContent = readFileSafe(dailyPath(yesterday));
|
|
260
|
+
if (yesterdayContent?.trim()) {
|
|
261
|
+
const section = formatContextSection(`## Daily log: ${yesterday} (yesterday)`, yesterdayContent, "end", CONTEXT_DAILY_MAX_LINES, CONTEXT_DAILY_MAX_CHARS);
|
|
262
|
+
if (section)
|
|
263
|
+
sections.push(section);
|
|
264
|
+
}
|
|
265
|
+
if (sections.length === 0) {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
const context = `# Memory\n\n${sections.join("\n\n---\n\n")}`;
|
|
269
|
+
if (context.length > CONTEXT_MAX_CHARS) {
|
|
270
|
+
const result = buildPreview(context, {
|
|
271
|
+
maxLines: Number.POSITIVE_INFINITY,
|
|
272
|
+
maxChars: CONTEXT_MAX_CHARS,
|
|
273
|
+
mode: "start",
|
|
274
|
+
});
|
|
275
|
+
const note = result.truncated
|
|
276
|
+
? `\n\n[truncated overall context: showing ${result.previewChars}/${result.totalChars} chars]`
|
|
277
|
+
: "";
|
|
278
|
+
return `${result.preview}${note}`;
|
|
279
|
+
}
|
|
280
|
+
return context;
|
|
281
|
+
}
|
|
282
|
+
let execFileFn = execFile;
|
|
283
|
+
let qmdAvailable = false;
|
|
284
|
+
let updateTimer = null;
|
|
285
|
+
/** QMD collection name — configurable per platform. */
|
|
286
|
+
let QMD_COLLECTION_NAME = "agent-memory";
|
|
287
|
+
/** Override execFile implementation (for testing). */
|
|
288
|
+
export function _setExecFileForTest(fn) {
|
|
289
|
+
execFileFn = fn;
|
|
290
|
+
}
|
|
291
|
+
/** Reset execFile implementation (for testing). */
|
|
292
|
+
export function _resetExecFileForTest() {
|
|
293
|
+
execFileFn = execFile;
|
|
294
|
+
}
|
|
295
|
+
/** Set qmd availability flag (for testing). */
|
|
296
|
+
export function _setQmdAvailable(value) {
|
|
297
|
+
qmdAvailable = value;
|
|
298
|
+
}
|
|
299
|
+
/** Get current qmd availability flag. */
|
|
300
|
+
export function _getQmdAvailable() {
|
|
301
|
+
return qmdAvailable;
|
|
302
|
+
}
|
|
303
|
+
/** Get current update timer (for testing). */
|
|
304
|
+
export function _getUpdateTimer() {
|
|
305
|
+
return updateTimer;
|
|
306
|
+
}
|
|
307
|
+
/** Clear the update timer (for testing). */
|
|
308
|
+
export function _clearUpdateTimer() {
|
|
309
|
+
if (updateTimer) {
|
|
310
|
+
clearTimeout(updateTimer);
|
|
311
|
+
updateTimer = null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** Get the current QMD collection name. */
|
|
315
|
+
export function getCollectionName() {
|
|
316
|
+
return QMD_COLLECTION_NAME;
|
|
317
|
+
}
|
|
318
|
+
/** Set the QMD collection name (for platform-specific overrides). */
|
|
319
|
+
export function setCollectionName(name) {
|
|
320
|
+
QMD_COLLECTION_NAME = name;
|
|
321
|
+
}
|
|
322
|
+
const QMD_REPO_URL = "https://github.com/tobi/qmd";
|
|
323
|
+
export function qmdInstallInstructions() {
|
|
324
|
+
return [
|
|
325
|
+
"memory_search requires qmd.",
|
|
326
|
+
"",
|
|
327
|
+
"Install qmd (requires Bun):",
|
|
328
|
+
` bun install -g ${QMD_REPO_URL}`,
|
|
329
|
+
" # ensure ~/.bun/bin is in your PATH",
|
|
330
|
+
"",
|
|
331
|
+
"Then set up the collection (one-time):",
|
|
332
|
+
` qmd collection add ${MEMORY_DIR} --name ${QMD_COLLECTION_NAME}`,
|
|
333
|
+
" qmd embed",
|
|
334
|
+
].join("\n");
|
|
335
|
+
}
|
|
336
|
+
export function qmdCollectionInstructions() {
|
|
337
|
+
return [
|
|
338
|
+
`qmd collection ${QMD_COLLECTION_NAME} is not configured.`,
|
|
339
|
+
"",
|
|
340
|
+
"Set up the collection (one-time):",
|
|
341
|
+
` qmd collection add ${MEMORY_DIR} --name ${QMD_COLLECTION_NAME}`,
|
|
342
|
+
" qmd embed",
|
|
343
|
+
].join("\n");
|
|
344
|
+
}
|
|
345
|
+
/** Auto-create the qmd collection and path contexts. */
|
|
346
|
+
export async function setupQmdCollection() {
|
|
347
|
+
try {
|
|
348
|
+
await new Promise((resolve, reject) => {
|
|
349
|
+
execFileFn("qmd", ["collection", "add", MEMORY_DIR, "--name", QMD_COLLECTION_NAME], { timeout: 10_000 }, (err) => (err ? reject(err) : resolve()));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// Collection may already exist under a different name — not critical
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
// Add path contexts (best-effort, ignore errors)
|
|
357
|
+
const contexts = [
|
|
358
|
+
["/daily", "Daily append-only work logs organized by date"],
|
|
359
|
+
["/", "Curated long-term memory: decisions, preferences, facts, lessons"],
|
|
360
|
+
];
|
|
361
|
+
for (const [ctxPath, desc] of contexts) {
|
|
362
|
+
try {
|
|
363
|
+
await new Promise((resolve, reject) => {
|
|
364
|
+
execFileFn("qmd", ["context", "add", ctxPath, desc, "-c", QMD_COLLECTION_NAME], { timeout: 10_000 }, (err) => (err ? reject(err) : resolve()));
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Ignore — context may already exist
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
export function detectQmd() {
|
|
374
|
+
return new Promise((resolve) => {
|
|
375
|
+
// qmd doesn't reliably support --version; use a fast command that exits 0 when available.
|
|
376
|
+
execFileFn("qmd", ["status"], { timeout: 5_000 }, (err) => {
|
|
377
|
+
resolve(!err);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
export function checkCollection(name) {
|
|
382
|
+
const collName = name ?? QMD_COLLECTION_NAME;
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
execFileFn("qmd", ["collection", "list", "--json"], { timeout: 10_000 }, (err, stdout) => {
|
|
385
|
+
if (err) {
|
|
386
|
+
resolve(false);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const collections = JSON.parse(stdout);
|
|
391
|
+
if (Array.isArray(collections)) {
|
|
392
|
+
resolve(collections.some((entry) => {
|
|
393
|
+
if (typeof entry === "string")
|
|
394
|
+
return entry === collName;
|
|
395
|
+
if (entry && typeof entry === "object" && "name" in entry) {
|
|
396
|
+
return entry.name === collName;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
// qmd may output an object with a collections array or similar
|
|
403
|
+
resolve(stdout.includes(collName));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// Fallback: just check if the name appears in the output
|
|
408
|
+
resolve(stdout.includes(collName));
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
export function getQmdUpdateMode() {
|
|
414
|
+
const mode = (process.env.AGENT_MEMORY_QMD_UPDATE ?? process.env.PI_MEMORY_QMD_UPDATE ?? "background").toLowerCase();
|
|
415
|
+
if (mode === "manual" || mode === "off" || mode === "background") {
|
|
416
|
+
return mode;
|
|
417
|
+
}
|
|
418
|
+
return "background";
|
|
419
|
+
}
|
|
420
|
+
export async function ensureQmdAvailableForUpdate() {
|
|
421
|
+
if (qmdAvailable)
|
|
422
|
+
return true;
|
|
423
|
+
if (getQmdUpdateMode() !== "background")
|
|
424
|
+
return false;
|
|
425
|
+
qmdAvailable = await detectQmd();
|
|
426
|
+
return qmdAvailable;
|
|
427
|
+
}
|
|
428
|
+
export function scheduleQmdUpdate() {
|
|
429
|
+
if (getQmdUpdateMode() !== "background")
|
|
430
|
+
return;
|
|
431
|
+
if (!qmdAvailable)
|
|
432
|
+
return;
|
|
433
|
+
if (updateTimer)
|
|
434
|
+
clearTimeout(updateTimer);
|
|
435
|
+
updateTimer = setTimeout(() => {
|
|
436
|
+
updateTimer = null;
|
|
437
|
+
execFileFn("qmd", ["update"], { timeout: 30_000 }, () => { });
|
|
438
|
+
}, 500);
|
|
439
|
+
}
|
|
440
|
+
export async function runQmdUpdateNow() {
|
|
441
|
+
if (getQmdUpdateMode() !== "background")
|
|
442
|
+
return;
|
|
443
|
+
if (!qmdAvailable)
|
|
444
|
+
return;
|
|
445
|
+
await new Promise((resolve) => {
|
|
446
|
+
execFileFn("qmd", ["update"], { timeout: 30_000 }, () => resolve());
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/** Search for memories relevant to the user's prompt. Returns formatted markdown or empty string on error. */
|
|
450
|
+
export async function searchRelevantMemories(prompt) {
|
|
451
|
+
if (!qmdAvailable || !prompt.trim())
|
|
452
|
+
return "";
|
|
453
|
+
// Sanitize: strip control chars, limit to 200 chars for the search query
|
|
454
|
+
const sanitized = prompt
|
|
455
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: we intentionally strip control chars.
|
|
456
|
+
.replace(/[\x00-\x1f\x7f]/g, " ")
|
|
457
|
+
.trim()
|
|
458
|
+
.slice(0, 200);
|
|
459
|
+
if (!sanitized)
|
|
460
|
+
return "";
|
|
461
|
+
try {
|
|
462
|
+
const hasCollection = await checkCollection();
|
|
463
|
+
if (!hasCollection)
|
|
464
|
+
return "";
|
|
465
|
+
const results = await Promise.race([
|
|
466
|
+
runQmdSearch("keyword", sanitized, 3),
|
|
467
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3_000)),
|
|
468
|
+
]);
|
|
469
|
+
if (!results || results.results.length === 0)
|
|
470
|
+
return "";
|
|
471
|
+
const snippets = results.results
|
|
472
|
+
.map((r) => {
|
|
473
|
+
const text = getQmdResultText(r);
|
|
474
|
+
if (!text.trim())
|
|
475
|
+
return null;
|
|
476
|
+
const filePath = getQmdResultPath(r);
|
|
477
|
+
const filePart = filePath ? `_${filePath}_` : "";
|
|
478
|
+
return filePart ? `${filePart}\n${text.trim()}` : text.trim();
|
|
479
|
+
})
|
|
480
|
+
.filter(Boolean);
|
|
481
|
+
if (snippets.length === 0)
|
|
482
|
+
return "";
|
|
483
|
+
return snippets.join("\n\n---\n\n");
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return "";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
export function getQmdResultPath(r) {
|
|
490
|
+
return r.path ?? r.file;
|
|
491
|
+
}
|
|
492
|
+
export function getQmdResultText(r) {
|
|
493
|
+
return r.content ?? r.chunk ?? r.snippet ?? "";
|
|
494
|
+
}
|
|
495
|
+
function stripAnsi(text) {
|
|
496
|
+
// qmd may emit spinners/progress bars even with --json, especially on first model download.
|
|
497
|
+
// Strip ANSI CSI/OSC sequences so we can reliably find and parse JSON payloads.
|
|
498
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escape sequences
|
|
499
|
+
return text.replace(/\u001b\[[0-9;]*[A-Za-z]/g, "").replace(/\u001b\][^\u0007]*(\u0007|\u001b\\)/g, "");
|
|
500
|
+
}
|
|
501
|
+
function parseQmdJson(stdout) {
|
|
502
|
+
const trimmed = stdout.trim();
|
|
503
|
+
if (!trimmed)
|
|
504
|
+
return [];
|
|
505
|
+
if (trimmed === "No results found." || trimmed === "No results found")
|
|
506
|
+
return [];
|
|
507
|
+
const cleaned = stripAnsi(stdout);
|
|
508
|
+
const lines = cleaned.split(/\r?\n/);
|
|
509
|
+
const startLine = lines.findIndex((l) => {
|
|
510
|
+
const s = l.trimStart();
|
|
511
|
+
return s.startsWith("[") || s.startsWith("{");
|
|
512
|
+
});
|
|
513
|
+
if (startLine === -1) {
|
|
514
|
+
throw new Error(`Failed to parse qmd output: ${trimmed.slice(0, 200)}`);
|
|
515
|
+
}
|
|
516
|
+
const jsonText = lines.slice(startLine).join("\n").trim();
|
|
517
|
+
if (!jsonText)
|
|
518
|
+
return [];
|
|
519
|
+
return JSON.parse(jsonText);
|
|
520
|
+
}
|
|
521
|
+
export function runQmdSearch(mode, query, limit) {
|
|
522
|
+
const subcommand = mode === "keyword" ? "search" : mode === "semantic" ? "vsearch" : "query";
|
|
523
|
+
const args = [subcommand, "--json", "-c", QMD_COLLECTION_NAME, "-n", String(limit), query];
|
|
524
|
+
return new Promise((resolve, reject) => {
|
|
525
|
+
execFileFn("qmd", args, { timeout: 60_000 }, (err, stdout, stderr) => {
|
|
526
|
+
if (err) {
|
|
527
|
+
reject(new Error(stderr?.trim() || err.message));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const parsed = parseQmdJson(stdout);
|
|
532
|
+
const results = Array.isArray(parsed) ? parsed : (parsed.results ?? parsed.hits ?? []);
|
|
533
|
+
resolve({ results, stderr: stderr ?? "" });
|
|
534
|
+
}
|
|
535
|
+
catch (parseErr) {
|
|
536
|
+
if (parseErr instanceof Error) {
|
|
537
|
+
reject(parseErr);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
reject(new Error(`Failed to parse qmd output: ${stdout.slice(0, 200)}`));
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
export async function memoryWrite(params) {
|
|
546
|
+
ensureDirs();
|
|
547
|
+
const { target, content, mode } = params;
|
|
548
|
+
const sid = shortSessionId(params.sessionId ?? "cli");
|
|
549
|
+
const ts = nowTimestamp();
|
|
550
|
+
if (target === "daily") {
|
|
551
|
+
const filePath = dailyPath(todayStr());
|
|
552
|
+
const existing = readFileSafe(filePath) ?? "";
|
|
553
|
+
const existingPreview = buildPreview(existing, {
|
|
554
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
555
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
556
|
+
mode: "end",
|
|
557
|
+
});
|
|
558
|
+
const existingSnippet = existingPreview.preview
|
|
559
|
+
? `\n\n${formatPreviewBlock("Existing daily log preview", existing, "end")}`
|
|
560
|
+
: "\n\nDaily log was empty.";
|
|
561
|
+
const separator = existing.trim() ? "\n\n" : "";
|
|
562
|
+
const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
|
|
563
|
+
fs.writeFileSync(filePath, existing + separator + stamped, "utf-8");
|
|
564
|
+
await ensureQmdAvailableForUpdate();
|
|
565
|
+
scheduleQmdUpdate();
|
|
566
|
+
return {
|
|
567
|
+
text: `Appended to daily log: ${filePath}${existingSnippet}`,
|
|
568
|
+
details: {
|
|
569
|
+
path: filePath,
|
|
570
|
+
target,
|
|
571
|
+
mode: "append",
|
|
572
|
+
sessionId: sid,
|
|
573
|
+
timestamp: ts,
|
|
574
|
+
qmdUpdateMode: getQmdUpdateMode(),
|
|
575
|
+
existingPreview,
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
// long_term
|
|
580
|
+
const memFile = getMemoryFile();
|
|
581
|
+
const existing = readFileSafe(memFile) ?? "";
|
|
582
|
+
const existingPreview = buildPreview(existing, {
|
|
583
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
584
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
585
|
+
mode: "middle",
|
|
586
|
+
});
|
|
587
|
+
const existingSnippet = existingPreview.preview
|
|
588
|
+
? `\n\n${formatPreviewBlock("Existing MEMORY.md preview", existing, "middle")}`
|
|
589
|
+
: "\n\nMEMORY.md was empty.";
|
|
590
|
+
if (mode === "overwrite") {
|
|
591
|
+
const stamped = `<!-- last updated: ${ts} [${sid}] -->\n${content}`;
|
|
592
|
+
fs.writeFileSync(memFile, stamped, "utf-8");
|
|
593
|
+
await ensureQmdAvailableForUpdate();
|
|
594
|
+
scheduleQmdUpdate();
|
|
595
|
+
return {
|
|
596
|
+
text: `Overwrote MEMORY.md${existingSnippet}`,
|
|
597
|
+
details: {
|
|
598
|
+
path: memFile,
|
|
599
|
+
target,
|
|
600
|
+
mode: "overwrite",
|
|
601
|
+
sessionId: sid,
|
|
602
|
+
timestamp: ts,
|
|
603
|
+
qmdUpdateMode: getQmdUpdateMode(),
|
|
604
|
+
existingPreview,
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
// append (default)
|
|
609
|
+
const separator = existing.trim() ? "\n\n" : "";
|
|
610
|
+
const stamped = `<!-- ${ts} [${sid}] -->\n${content}`;
|
|
611
|
+
fs.writeFileSync(memFile, existing + separator + stamped, "utf-8");
|
|
612
|
+
await ensureQmdAvailableForUpdate();
|
|
613
|
+
scheduleQmdUpdate();
|
|
614
|
+
return {
|
|
615
|
+
text: `Appended to MEMORY.md${existingSnippet}`,
|
|
616
|
+
details: {
|
|
617
|
+
path: memFile,
|
|
618
|
+
target,
|
|
619
|
+
mode: "append",
|
|
620
|
+
sessionId: sid,
|
|
621
|
+
timestamp: ts,
|
|
622
|
+
qmdUpdateMode: getQmdUpdateMode(),
|
|
623
|
+
existingPreview,
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
export async function scratchpadAction(params) {
|
|
628
|
+
ensureDirs();
|
|
629
|
+
const { action, text } = params;
|
|
630
|
+
const sid = shortSessionId(params.sessionId ?? "cli");
|
|
631
|
+
const ts = nowTimestamp();
|
|
632
|
+
const spFile = getScratchpadFile();
|
|
633
|
+
const existing = readFileSafe(spFile) ?? "";
|
|
634
|
+
let items = parseScratchpad(existing);
|
|
635
|
+
if (action === "list") {
|
|
636
|
+
if (items.length === 0) {
|
|
637
|
+
return { text: "Scratchpad is empty.", details: {} };
|
|
638
|
+
}
|
|
639
|
+
const serialized = serializeScratchpad(items);
|
|
640
|
+
const preview = buildPreview(serialized, {
|
|
641
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
642
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
643
|
+
mode: "start",
|
|
644
|
+
});
|
|
645
|
+
return {
|
|
646
|
+
text: formatPreviewBlock("Scratchpad preview", serialized, "start"),
|
|
647
|
+
details: {
|
|
648
|
+
count: items.length,
|
|
649
|
+
open: items.filter((i) => !i.done).length,
|
|
650
|
+
preview,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
if (action === "add") {
|
|
655
|
+
if (!text) {
|
|
656
|
+
return { text: "Error: 'text' is required for add.", details: {} };
|
|
657
|
+
}
|
|
658
|
+
items.push({ done: false, text, meta: `<!-- ${ts} [${sid}] -->` });
|
|
659
|
+
const serialized = serializeScratchpad(items);
|
|
660
|
+
const preview = buildPreview(serialized, {
|
|
661
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
662
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
663
|
+
mode: "start",
|
|
664
|
+
});
|
|
665
|
+
fs.writeFileSync(spFile, serialized, "utf-8");
|
|
666
|
+
await ensureQmdAvailableForUpdate();
|
|
667
|
+
scheduleQmdUpdate();
|
|
668
|
+
return {
|
|
669
|
+
text: `Added: - [ ] ${text}\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
|
|
670
|
+
details: {
|
|
671
|
+
action,
|
|
672
|
+
sessionId: sid,
|
|
673
|
+
timestamp: ts,
|
|
674
|
+
qmdUpdateMode: getQmdUpdateMode(),
|
|
675
|
+
preview,
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
if (action === "done" || action === "undo") {
|
|
680
|
+
if (!text) {
|
|
681
|
+
return { text: `Error: 'text' is required for ${action}.`, details: {} };
|
|
682
|
+
}
|
|
683
|
+
const needle = text.toLowerCase();
|
|
684
|
+
const targetDone = action === "done";
|
|
685
|
+
let matched = false;
|
|
686
|
+
for (const item of items) {
|
|
687
|
+
if (item.done !== targetDone && item.text.toLowerCase().includes(needle)) {
|
|
688
|
+
item.done = targetDone;
|
|
689
|
+
matched = true;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!matched) {
|
|
694
|
+
return {
|
|
695
|
+
text: `No matching ${targetDone ? "open" : "done"} item found for: "${text}"`,
|
|
696
|
+
details: {},
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
const serialized = serializeScratchpad(items);
|
|
700
|
+
const preview = buildPreview(serialized, {
|
|
701
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
702
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
703
|
+
mode: "start",
|
|
704
|
+
});
|
|
705
|
+
fs.writeFileSync(spFile, serialized, "utf-8");
|
|
706
|
+
await ensureQmdAvailableForUpdate();
|
|
707
|
+
scheduleQmdUpdate();
|
|
708
|
+
return {
|
|
709
|
+
text: `Updated.\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
|
|
710
|
+
details: {
|
|
711
|
+
action,
|
|
712
|
+
sessionId: sid,
|
|
713
|
+
timestamp: ts,
|
|
714
|
+
qmdUpdateMode: getQmdUpdateMode(),
|
|
715
|
+
preview,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (action === "clear_done") {
|
|
720
|
+
const before = items.length;
|
|
721
|
+
items = items.filter((i) => !i.done);
|
|
722
|
+
const removed = before - items.length;
|
|
723
|
+
const serialized = serializeScratchpad(items);
|
|
724
|
+
const preview = buildPreview(serialized, {
|
|
725
|
+
maxLines: RESPONSE_PREVIEW_MAX_LINES,
|
|
726
|
+
maxChars: RESPONSE_PREVIEW_MAX_CHARS,
|
|
727
|
+
mode: "start",
|
|
728
|
+
});
|
|
729
|
+
fs.writeFileSync(spFile, serialized, "utf-8");
|
|
730
|
+
await ensureQmdAvailableForUpdate();
|
|
731
|
+
scheduleQmdUpdate();
|
|
732
|
+
return {
|
|
733
|
+
text: `Cleared ${removed} done item(s).\n\n${formatPreviewBlock("Scratchpad preview", serialized, "start")}`,
|
|
734
|
+
details: {
|
|
735
|
+
action,
|
|
736
|
+
removed,
|
|
737
|
+
qmdUpdateMode: getQmdUpdateMode(),
|
|
738
|
+
preview,
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
return { text: `Unknown action: ${action}`, details: {} };
|
|
743
|
+
}
|
|
744
|
+
export async function memoryRead(params) {
|
|
745
|
+
ensureDirs();
|
|
746
|
+
const { target, date } = params;
|
|
747
|
+
if (target === "list") {
|
|
748
|
+
try {
|
|
749
|
+
const files = fs
|
|
750
|
+
.readdirSync(getDailyDir())
|
|
751
|
+
.filter((f) => f.endsWith(".md"))
|
|
752
|
+
.sort()
|
|
753
|
+
.reverse();
|
|
754
|
+
if (files.length === 0) {
|
|
755
|
+
return { text: "No daily logs found.", details: {} };
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
text: `Daily logs:\n${files.map((f) => `- ${f}`).join("\n")}`,
|
|
759
|
+
details: { files },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
return { text: "No daily logs directory.", details: {} };
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (target === "daily") {
|
|
767
|
+
const d = date ?? todayStr();
|
|
768
|
+
const filePath = dailyPath(d);
|
|
769
|
+
const content = readFileSafe(filePath);
|
|
770
|
+
if (!content) {
|
|
771
|
+
return { text: `No daily log for ${d}.`, details: {} };
|
|
772
|
+
}
|
|
773
|
+
return { text: content, details: { path: filePath, date: d } };
|
|
774
|
+
}
|
|
775
|
+
if (target === "scratchpad") {
|
|
776
|
+
const content = readFileSafe(getScratchpadFile());
|
|
777
|
+
if (!content?.trim()) {
|
|
778
|
+
return {
|
|
779
|
+
text: "SCRATCHPAD.md is empty or does not exist.",
|
|
780
|
+
details: {},
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return { text: content, details: { path: getScratchpadFile() } };
|
|
784
|
+
}
|
|
785
|
+
// long_term
|
|
786
|
+
const content = readFileSafe(getMemoryFile());
|
|
787
|
+
if (!content) {
|
|
788
|
+
return {
|
|
789
|
+
text: "MEMORY.md is empty or does not exist.",
|
|
790
|
+
details: {},
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
return { text: content, details: { path: getMemoryFile() } };
|
|
794
|
+
}
|
|
795
|
+
export async function memorySearch(params) {
|
|
796
|
+
let isAvailable = qmdAvailable;
|
|
797
|
+
if (!isAvailable) {
|
|
798
|
+
const found = await detectQmd();
|
|
799
|
+
_setQmdAvailable(found);
|
|
800
|
+
isAvailable = found;
|
|
801
|
+
}
|
|
802
|
+
if (!isAvailable) {
|
|
803
|
+
return {
|
|
804
|
+
text: qmdInstallInstructions(),
|
|
805
|
+
details: {},
|
|
806
|
+
isError: true,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
const collName = QMD_COLLECTION_NAME;
|
|
810
|
+
let hasCollection = await checkCollection(collName);
|
|
811
|
+
if (!hasCollection) {
|
|
812
|
+
const created = await setupQmdCollection();
|
|
813
|
+
if (created) {
|
|
814
|
+
hasCollection = true;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
if (!hasCollection) {
|
|
818
|
+
return {
|
|
819
|
+
text: `Could not set up qmd ${collName} collection. Check that qmd is working and the memory directory exists.`,
|
|
820
|
+
details: {},
|
|
821
|
+
isError: true,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const mode = params.mode ?? "keyword";
|
|
825
|
+
const limit = params.limit ?? 5;
|
|
826
|
+
try {
|
|
827
|
+
const { results, stderr } = await runQmdSearch(mode, params.query, limit);
|
|
828
|
+
const needsEmbed = /need embeddings/i.test(stderr ?? "");
|
|
829
|
+
if (results.length === 0) {
|
|
830
|
+
if (needsEmbed && (mode === "semantic" || mode === "deep")) {
|
|
831
|
+
return {
|
|
832
|
+
text: [
|
|
833
|
+
`No results found for "${params.query}" (mode: ${mode}).`,
|
|
834
|
+
"",
|
|
835
|
+
"qmd reports missing vector embeddings for one or more documents.",
|
|
836
|
+
"Run this once, then retry:",
|
|
837
|
+
" qmd embed",
|
|
838
|
+
].join("\n"),
|
|
839
|
+
details: {
|
|
840
|
+
mode,
|
|
841
|
+
query: params.query,
|
|
842
|
+
count: 0,
|
|
843
|
+
needsEmbed: true,
|
|
844
|
+
},
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
text: `No results found for "${params.query}" (mode: ${mode}).`,
|
|
849
|
+
details: { mode, query: params.query, count: 0, needsEmbed },
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
const formatted = results
|
|
853
|
+
.map((r, i) => {
|
|
854
|
+
const parts = [`### Result ${i + 1}`];
|
|
855
|
+
const filePath = getQmdResultPath(r);
|
|
856
|
+
if (filePath)
|
|
857
|
+
parts.push(`**File:** ${filePath}`);
|
|
858
|
+
if (r.score != null)
|
|
859
|
+
parts.push(`**Score:** ${r.score}`);
|
|
860
|
+
const text = getQmdResultText(r);
|
|
861
|
+
if (text)
|
|
862
|
+
parts.push(`\n${text}`);
|
|
863
|
+
return parts.join("\n");
|
|
864
|
+
})
|
|
865
|
+
.join("\n\n---\n\n");
|
|
866
|
+
return {
|
|
867
|
+
text: formatted,
|
|
868
|
+
details: {
|
|
869
|
+
mode,
|
|
870
|
+
query: params.query,
|
|
871
|
+
count: results.length,
|
|
872
|
+
needsEmbed,
|
|
873
|
+
},
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
return {
|
|
878
|
+
text: `memory_search error: ${err instanceof Error ? err.message : String(err)}`,
|
|
879
|
+
details: {},
|
|
880
|
+
isError: true,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|