@nowledge/openclaw-nowledge-mem 0.2.7
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/CHANGELOG.md +211 -0
- package/README.md +191 -0
- package/openclaw.plugin.json +61 -0
- package/package.json +54 -0
- package/src/client.js +673 -0
- package/src/commands/cli.js +45 -0
- package/src/commands/slash.js +109 -0
- package/src/config.js +43 -0
- package/src/hooks/capture.js +337 -0
- package/src/hooks/recall.js +109 -0
- package/src/index.js +81 -0
- package/src/tools/connections.js +324 -0
- package/src/tools/context.js +126 -0
- package/src/tools/forget.js +154 -0
- package/src/tools/memory-get.js +168 -0
- package/src/tools/memory-search.js +183 -0
- package/src/tools/save.js +179 -0
- package/src/tools/timeline.js +208 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nowledge_mem_connections — explore the knowledge graph around a topic or memory.
|
|
3
|
+
*
|
|
4
|
+
* This is Nowledge Mem's graph-native differentiator. Instead of isolated
|
|
5
|
+
* vector search, it traverses EVOLVES chains, entity relationships,
|
|
6
|
+
* source provenance (SOURCED_FROM), and memory-memory connections to
|
|
7
|
+
* surface knowledge the user didn't know was connected.
|
|
8
|
+
*
|
|
9
|
+
* The graph contains:
|
|
10
|
+
* - Memory nodes (knowledge units with 8 types)
|
|
11
|
+
* - Entity nodes (people, technologies, projects)
|
|
12
|
+
* - Source nodes (documents, URLs — the Library)
|
|
13
|
+
* - EVOLVES edges (how understanding grows over time: replaces, enriches, confirms, challenges)
|
|
14
|
+
* - CRYSTALLIZED_FROM edges (crystal ← source memories it synthesized)
|
|
15
|
+
* - SOURCED_FROM edges (memory ← document it was extracted from)
|
|
16
|
+
* - MENTIONS edges (which entities a memory references)
|
|
17
|
+
*
|
|
18
|
+
* Each connection is shown WITH its relationship type and strength.
|
|
19
|
+
*
|
|
20
|
+
* Uses the local API directly (graph CLI isn't available yet).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const EDGE_TYPE_LABELS = {
|
|
24
|
+
CRYSTALLIZED_FROM: "crystallized from",
|
|
25
|
+
EVOLVES: "knowledge evolution",
|
|
26
|
+
SOURCED_FROM: "sourced from document",
|
|
27
|
+
MENTIONS: "mentions entity",
|
|
28
|
+
RELATED: "related",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const EVOLVES_RELATION_LABELS = {
|
|
32
|
+
replaces: "supersedes — newer understanding replaces older",
|
|
33
|
+
enriches: "enriches — adds depth to earlier knowledge",
|
|
34
|
+
confirms: "confirms — corroborated from another source",
|
|
35
|
+
challenges: "challenges — contradicts or questions",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function getNodeTitle(node) {
|
|
39
|
+
return (
|
|
40
|
+
node.label ||
|
|
41
|
+
node.metadata?.title ||
|
|
42
|
+
node.title ||
|
|
43
|
+
node.original_name ||
|
|
44
|
+
node.name ||
|
|
45
|
+
"(untitled)"
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getNodeSnippet(node) {
|
|
50
|
+
const content =
|
|
51
|
+
node.metadata?.content || node.content || node.summary || node.description || "";
|
|
52
|
+
return content.slice(0, 150);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getNodeType(node) {
|
|
56
|
+
return node.node_type || node.type || "memory";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a map from node id → node for efficient edge→node joining
|
|
61
|
+
*/
|
|
62
|
+
function buildNodeMap(nodes) {
|
|
63
|
+
const map = new Map();
|
|
64
|
+
for (const n of nodes) {
|
|
65
|
+
map.set(n.id, n);
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Group edges by type, pairing each with the connected node.
|
|
72
|
+
* Returns { edgeType → [{ node, edge }] }
|
|
73
|
+
*/
|
|
74
|
+
function groupConnectionsByEdgeType(edges, nodeMap, centerId) {
|
|
75
|
+
const groups = new Map();
|
|
76
|
+
|
|
77
|
+
for (const edge of edges) {
|
|
78
|
+
const neighborId =
|
|
79
|
+
edge.source === centerId ? edge.target : edge.source;
|
|
80
|
+
const node = nodeMap.get(neighborId);
|
|
81
|
+
if (!node) continue;
|
|
82
|
+
|
|
83
|
+
const edgeType = edge.edge_type || edge.type || "RELATED";
|
|
84
|
+
if (!groups.has(edgeType)) groups.set(edgeType, []);
|
|
85
|
+
|
|
86
|
+
groups.get(edgeType).push({
|
|
87
|
+
node,
|
|
88
|
+
edge,
|
|
89
|
+
neighborId,
|
|
90
|
+
relation: edge.metadata?.relation_type || edge.content_relation || null,
|
|
91
|
+
weight: edge.weight ?? edge.relevance_score ?? 0.5,
|
|
92
|
+
label: edge.label || EDGE_TYPE_LABELS[edgeType] || edgeType,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return groups;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createConnectionsTool(client, logger) {
|
|
100
|
+
return {
|
|
101
|
+
name: "nowledge_mem_connections",
|
|
102
|
+
description:
|
|
103
|
+
"Explore the knowledge graph around a memory or topic. Use this for:\n" +
|
|
104
|
+
"(1) Cross-topic synthesis — 'How does my UV setup relate to my Docker notes?'\n" +
|
|
105
|
+
"(2) Source provenance — which document (PDF, DOCX) or URL this knowledge was extracted from\n" +
|
|
106
|
+
"(3) Knowledge evolution — how understanding grew or changed over time (EVOLVES chains)\n" +
|
|
107
|
+
"(4) Crystal breakdown — which source memories a crystal was synthesized from\n" +
|
|
108
|
+
"(5) Entity discovery — people, projects, and tools clustered around this topic\n" +
|
|
109
|
+
"Each connection shows its relationship type and strength.\n" +
|
|
110
|
+
"Tip: call memory_search first to get a memoryId, then pass it here for deep exploration.",
|
|
111
|
+
parameters: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
memoryId: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description:
|
|
117
|
+
"Memory ID to explore connections from (from memory_search results)",
|
|
118
|
+
},
|
|
119
|
+
query: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description:
|
|
122
|
+
"Topic to explore (searches first, then expands the top result's neighborhood)",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
async execute(_toolCallId, params) {
|
|
127
|
+
const safeParams = params && typeof params === "object" ? params : {};
|
|
128
|
+
const memoryId = safeParams.memoryId
|
|
129
|
+
? String(safeParams.memoryId).trim()
|
|
130
|
+
: "";
|
|
131
|
+
const query = safeParams.query ? String(safeParams.query).trim() : "";
|
|
132
|
+
|
|
133
|
+
if (!memoryId && !query) {
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: "Provide memoryId or query to explore connections.",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let targetId = memoryId;
|
|
145
|
+
|
|
146
|
+
// If query provided, search first to find the best entry point
|
|
147
|
+
if (!targetId && query) {
|
|
148
|
+
try {
|
|
149
|
+
const results = await client.search(query, 1);
|
|
150
|
+
if (results.length === 0) {
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: "text",
|
|
155
|
+
text: `No memories found for "${query}" to explore connections from.`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
targetId = results[0].id;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `Search failed: ${msg}` }],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sections = [];
|
|
170
|
+
|
|
171
|
+
// 1. Graph neighbors (connected memories, sources, entities) — with edge types joined
|
|
172
|
+
try {
|
|
173
|
+
const neighborsData = await client.graphExpand(targetId, {
|
|
174
|
+
depth: 1,
|
|
175
|
+
limit: 20,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const neighbors = neighborsData.neighbors || [];
|
|
179
|
+
const edges = neighborsData.edges || [];
|
|
180
|
+
const nodeMap = buildNodeMap(neighbors);
|
|
181
|
+
const grouped = groupConnectionsByEdgeType(edges, nodeMap, targetId);
|
|
182
|
+
|
|
183
|
+
// --- CRYSTALLIZED_FROM: crystal's source memories ---
|
|
184
|
+
const crystalConns = grouped.get("CRYSTALLIZED_FROM") || [];
|
|
185
|
+
if (crystalConns.length > 0) {
|
|
186
|
+
const lines = crystalConns.map(({ node, weight }) => {
|
|
187
|
+
const title = getNodeTitle(node);
|
|
188
|
+
const snippet = getNodeSnippet(node);
|
|
189
|
+
const strength = weight ? ` [${(weight * 100).toFixed(0)}%]` : "";
|
|
190
|
+
return ` - ${title}${strength}${snippet ? `: ${snippet}` : ""}\n → id: ${node.id}`;
|
|
191
|
+
});
|
|
192
|
+
sections.push(
|
|
193
|
+
`Synthesized from ${crystalConns.length} source memor${crystalConns.length === 1 ? "y" : "ies"}:\n${lines.join("\n")}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// --- EVOLVES: knowledge evolution edges ---
|
|
198
|
+
const evolvesConns = grouped.get("EVOLVES") || [];
|
|
199
|
+
if (evolvesConns.length > 0) {
|
|
200
|
+
const lines = evolvesConns.map(({ node, relation }) => {
|
|
201
|
+
const title = getNodeTitle(node);
|
|
202
|
+
const relLabel =
|
|
203
|
+
EVOLVES_RELATION_LABELS[relation] ||
|
|
204
|
+
relation ||
|
|
205
|
+
"related knowledge";
|
|
206
|
+
return ` - ${relLabel}\n "${title}"\n → id: ${node.id}`;
|
|
207
|
+
});
|
|
208
|
+
sections.push(`Knowledge evolution:\n${lines.join("\n")}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- SOURCED_FROM: document provenance ---
|
|
212
|
+
const sourceConns = grouped.get("SOURCED_FROM") || [];
|
|
213
|
+
if (sourceConns.length > 0) {
|
|
214
|
+
const lines = sourceConns.map(({ node }) => {
|
|
215
|
+
const name = getNodeTitle(node);
|
|
216
|
+
const sourceType =
|
|
217
|
+
node.metadata?.source_type || node.source_type || "document";
|
|
218
|
+
return ` - ${name} (${sourceType}) → id: ${node.id}`;
|
|
219
|
+
});
|
|
220
|
+
sections.push(
|
|
221
|
+
`Sourced from document${sourceConns.length > 1 ? "s" : ""}:\n${lines.join("\n")}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- MENTIONS: entity connections ---
|
|
226
|
+
const mentionConns = grouped.get("MENTIONS") || [];
|
|
227
|
+
if (mentionConns.length > 0) {
|
|
228
|
+
const lines = mentionConns.map(({ node }) => {
|
|
229
|
+
const name = getNodeTitle(node);
|
|
230
|
+
const type = getNodeType(node);
|
|
231
|
+
return ` - ${name} (${type})`;
|
|
232
|
+
});
|
|
233
|
+
sections.push(`Entities mentioned:\n${lines.join("\n")}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Other memory connections (RELATED, etc.) ---
|
|
237
|
+
for (const [edgeType, conns] of grouped.entries()) {
|
|
238
|
+
if (
|
|
239
|
+
["CRYSTALLIZED_FROM", "EVOLVES", "SOURCED_FROM", "MENTIONS"].includes(
|
|
240
|
+
edgeType,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
continue;
|
|
244
|
+
|
|
245
|
+
const memConns = conns.filter(
|
|
246
|
+
({ node }) =>
|
|
247
|
+
getNodeType(node) === "memory" || getNodeType(node) === "Memory",
|
|
248
|
+
);
|
|
249
|
+
if (memConns.length === 0) continue;
|
|
250
|
+
|
|
251
|
+
const lines = memConns.map(({ node, weight }) => {
|
|
252
|
+
const title = getNodeTitle(node);
|
|
253
|
+
const snippet = getNodeSnippet(node);
|
|
254
|
+
const strength = weight ? ` [${(weight * 100).toFixed(0)}%]` : "";
|
|
255
|
+
return ` - ${title}${strength}: ${snippet}\n → id: ${node.id}`;
|
|
256
|
+
});
|
|
257
|
+
const label =
|
|
258
|
+
EDGE_TYPE_LABELS[edgeType] ||
|
|
259
|
+
edgeType.toLowerCase().replace(/_/g, " ");
|
|
260
|
+
sections.push(
|
|
261
|
+
`Connected via "${label}" (${memConns.length}):\n${lines.join("\n")}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (neighbors.length === 0) {
|
|
266
|
+
sections.push(
|
|
267
|
+
"No direct connections yet — connections form as the Knowledge Agent processes related memories.",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
272
|
+
logger.warn(`connections: graph expand failed: ${msg}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 2. EVOLVES chain (full version history) — CLI v0.4.1+ / API fallback
|
|
276
|
+
try {
|
|
277
|
+
const evolveData = await client.graphEvolves(targetId, { limit: 10 });
|
|
278
|
+
const edges = evolveData?.edges ?? [];
|
|
279
|
+
|
|
280
|
+
if (edges.length > 0) {
|
|
281
|
+
const lines = edges.map((edge) => {
|
|
282
|
+
const relation = edge.content_relation || "";
|
|
283
|
+
const relLabel = EVOLVES_RELATION_LABELS[relation] || relation || "";
|
|
284
|
+
// Show the "other" node relative to our target
|
|
285
|
+
const isOlderNode = edge.older_id === targetId;
|
|
286
|
+
const otherTitle = isOlderNode
|
|
287
|
+
? (edge.newer_title || "(untitled)")
|
|
288
|
+
: (edge.older_title || "(untitled)");
|
|
289
|
+
const direction = isOlderNode ? "→" : "←";
|
|
290
|
+
return ` ${direction} ${otherTitle}${relLabel ? ` — ${relLabel}` : ""}`;
|
|
291
|
+
});
|
|
292
|
+
sections.push(`Knowledge evolution:\n${lines.join("\n")}`);
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
// No EVOLVES chain for this memory — normal
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (sections.length === 0 || (sections.length === 1 && sections[0].includes("No direct connections"))) {
|
|
299
|
+
return {
|
|
300
|
+
content: [
|
|
301
|
+
{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: `Memory ${targetId} has no graph connections yet. Connections form as the Knowledge Agent processes related knowledge.`,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const result = [
|
|
310
|
+
`Graph connections for: ${targetId}`,
|
|
311
|
+
"",
|
|
312
|
+
...sections,
|
|
313
|
+
].join("\n\n");
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
content: [{ type: "text", text: result }],
|
|
317
|
+
details: {
|
|
318
|
+
memoryId: targetId,
|
|
319
|
+
sectionCount: sections.length,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nowledge_mem_context — read today's Working Memory.
|
|
3
|
+
*
|
|
4
|
+
* Working Memory is Nowledge Mem's cognitive-science-grounded daily
|
|
5
|
+
* artifact: focus areas, unresolved flags, recent changes, and
|
|
6
|
+
* priorities — generated by the Knowledge Agent each morning and
|
|
7
|
+
* updated throughout the day as events occur.
|
|
8
|
+
*
|
|
9
|
+
* This is fundamentally different from a static "user profile."
|
|
10
|
+
* It's a living document that reflects what matters TODAY.
|
|
11
|
+
*/
|
|
12
|
+
export function createContextTool(client, logger) {
|
|
13
|
+
return {
|
|
14
|
+
name: "nowledge_mem_context",
|
|
15
|
+
description:
|
|
16
|
+
"Read or patch the user's Working Memory — today's focus areas, priorities, unresolved flags, and recent activity. " +
|
|
17
|
+
"Generated by the Knowledge Agent each morning and updated throughout the day as new knowledge arrives. " +
|
|
18
|
+
"READ MODE (default): Call with no parameters to understand what the user is currently focused on. " +
|
|
19
|
+
"PATCH MODE: Supply patch_section to update just one section without overwriting the rest. " +
|
|
20
|
+
"Use patch_content to replace the section body, or patch_append to add text to it. " +
|
|
21
|
+
"Example: patch_section='## Notes', patch_append='New note: ...' to add a note without losing existing content. " +
|
|
22
|
+
"Note: Working Memory is regenerated by the Knowledge Agent each morning. " +
|
|
23
|
+
"To see what happened recently by date, use nowledge_mem_timeline instead.",
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
patch_section: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description:
|
|
30
|
+
"Section heading to update (e.g. '## Notes', '## Focus Areas'). " +
|
|
31
|
+
"Case-insensitive partial match. Required for patch mode.",
|
|
32
|
+
},
|
|
33
|
+
patch_content: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description:
|
|
36
|
+
"Replace the entire section body with this markdown text. Use with patch_section.",
|
|
37
|
+
},
|
|
38
|
+
patch_append: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description:
|
|
41
|
+
"Append this text to the section body (preserves existing content). Use with patch_section.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
async execute(_toolCallId, params) {
|
|
46
|
+
const safeParams = params && typeof params === "object" ? params : {};
|
|
47
|
+
const patchSection = safeParams.patch_section
|
|
48
|
+
? String(safeParams.patch_section).trim()
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
// — PATCH MODE —
|
|
52
|
+
if (patchSection) {
|
|
53
|
+
const patchContent = safeParams.patch_content !== undefined
|
|
54
|
+
? String(safeParams.patch_content)
|
|
55
|
+
: undefined;
|
|
56
|
+
const patchAppend = safeParams.patch_append !== undefined
|
|
57
|
+
? String(safeParams.patch_append)
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
60
|
+
if (patchContent === undefined && patchAppend === undefined) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: "patch_section requires either patch_content (replace) or patch_append (append). Please provide one.",
|
|
65
|
+
}],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
await client.patchWorkingMemory(patchSection, {
|
|
71
|
+
content: patchContent,
|
|
72
|
+
append: patchAppend,
|
|
73
|
+
});
|
|
74
|
+
const action = patchAppend !== undefined ? "Appended to" : "Replaced";
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `Working Memory updated. ${action} section: "${patchSection}".`,
|
|
79
|
+
}],
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
83
|
+
logger.error(`context patch failed: ${msg}`);
|
|
84
|
+
return {
|
|
85
|
+
content: [{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Failed to patch Working Memory: ${msg}`,
|
|
88
|
+
}],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// — READ MODE —
|
|
94
|
+
try {
|
|
95
|
+
const wm = await client.readWorkingMemory();
|
|
96
|
+
|
|
97
|
+
if (!wm.available) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: "Working Memory not available yet. It is generated by the Knowledge Agent during the daily morning briefing. Ensure Nowledge Mem is running with Background Intelligence enabled.",
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: wm.content }],
|
|
110
|
+
details: { available: true },
|
|
111
|
+
};
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
114
|
+
logger.error(`context read failed: ${msg}`);
|
|
115
|
+
return {
|
|
116
|
+
content: [
|
|
117
|
+
{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: `Failed to read Working Memory: ${msg}`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nowledge_mem_forget — delete a memory by ID or by search query.
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* 1. Direct delete by memoryId (fast, deterministic)
|
|
6
|
+
* 2. Search-then-confirm by query (finds candidates, deletes if single high-confidence match)
|
|
7
|
+
*/
|
|
8
|
+
export function createForgetTool(client, logger) {
|
|
9
|
+
return {
|
|
10
|
+
name: "nowledge_mem_forget",
|
|
11
|
+
description:
|
|
12
|
+
"Delete a memory from your knowledge base. Provide memoryId for direct delete, or query to find and remove. If multiple matches, returns candidates for user confirmation.",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
memoryId: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description:
|
|
19
|
+
"Specific memory ID to delete (from memory_search results or /recall output)",
|
|
20
|
+
},
|
|
21
|
+
query: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description:
|
|
24
|
+
"Search query to find the memory to delete. If a single high-confidence match is found, it is deleted directly.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async execute(_toolCallId, params) {
|
|
29
|
+
const safeParams = params && typeof params === "object" ? params : {};
|
|
30
|
+
const memoryId = safeParams.memoryId
|
|
31
|
+
? String(safeParams.memoryId).trim()
|
|
32
|
+
: "";
|
|
33
|
+
const query = safeParams.query ? String(safeParams.query).trim() : "";
|
|
34
|
+
|
|
35
|
+
if (!memoryId && !query) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: "Provide memoryId or query to find the memory to delete.",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Mode 1: Direct delete by ID
|
|
47
|
+
if (memoryId) {
|
|
48
|
+
try {
|
|
49
|
+
client.exec(["--json", "m", "delete", "-f", memoryId]);
|
|
50
|
+
logger.info(`forget: deleted memory ${memoryId}`);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Memory ${memoryId} deleted.`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
details: { action: "deleted", id: memoryId },
|
|
59
|
+
};
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
logger.error(`forget by id failed: ${message}`);
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: `Failed to delete memory ${memoryId}: ${message}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Mode 2: Search-then-confirm
|
|
75
|
+
try {
|
|
76
|
+
const results = await client.search(query, 5);
|
|
77
|
+
|
|
78
|
+
if (results.length === 0) {
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
{
|
|
82
|
+
type: "text",
|
|
83
|
+
text: `No matching memories found for: "${query}"`,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
details: { found: 0 },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Single high-confidence match — delete directly
|
|
91
|
+
if (results.length === 1 || results[0].score >= 0.85) {
|
|
92
|
+
const target = results[0];
|
|
93
|
+
try {
|
|
94
|
+
client.exec(["--json", "m", "delete", "-f", target.id]);
|
|
95
|
+
logger.info(`forget: deleted memory ${target.id} via search`);
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `Deleted: "${target.title || target.content.slice(0, 60)}" (id: ${target.id})`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
details: { action: "deleted", id: target.id },
|
|
104
|
+
};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Found match but delete failed: ${message}`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Multiple candidates — return list for user to pick
|
|
119
|
+
const candidates = results.map(
|
|
120
|
+
(r, i) =>
|
|
121
|
+
`${i + 1}. ${r.title || "(untitled)"} (${(r.score * 100).toFixed(0)}%) — id: ${r.id}\n ${r.content.slice(0, 100)}`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: `Found ${results.length} candidates. Ask user which to delete, then call again with memoryId:\n\n${candidates.join("\n\n")}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
details: {
|
|
132
|
+
action: "candidates",
|
|
133
|
+
candidates: results.map((r) => ({
|
|
134
|
+
id: r.id,
|
|
135
|
+
title: r.title,
|
|
136
|
+
score: r.score,
|
|
137
|
+
})),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
logger.error(`forget search failed: ${message}`);
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `Failed to search for memory to delete: ${message}`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|