memory-search-plugin 0.4.0 → 0.6.0
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/index.js +413 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
package/index.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// index.ts
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
|
|
4
|
+
// gateway-client.ts
|
|
5
|
+
function createGatewayClient(config) {
|
|
6
|
+
const baseUrl = config.gatewayUrl.replace(/\/+$/, "");
|
|
7
|
+
const token = config.gatewayToken || "";
|
|
8
|
+
const authHeaders = token ? { Authorization: `Bearer ${token}` } : {};
|
|
9
|
+
return {
|
|
10
|
+
async callGatewaySearch(options) {
|
|
11
|
+
const url = `${baseUrl}/api/memory/search`;
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
15
|
+
body: JSON.stringify(options)
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
const errorBody = await response.text().catch(() => "");
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Gateway search failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return response.json();
|
|
24
|
+
},
|
|
25
|
+
async callGatewayGet(options) {
|
|
26
|
+
const url = `${baseUrl}/api/memory/get`;
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
30
|
+
body: JSON.stringify(options)
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorBody = await response.text().catch(() => "");
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Gateway get failed: ${response.status} ${response.statusText} ${errorBody}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return response.json();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// identity.ts
|
|
44
|
+
function resolveIdentityFromParams(params) {
|
|
45
|
+
const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE;
|
|
46
|
+
if (testScene) {
|
|
47
|
+
const result = {
|
|
48
|
+
scene: testScene,
|
|
49
|
+
user_id: process.env.MEMORY_GATEWAY_TEST_USER_ID || "owner_A",
|
|
50
|
+
group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null
|
|
51
|
+
};
|
|
52
|
+
console.log("[memory-search-plugin] TEST MODE identity:", JSON.stringify(result));
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
const userId = params.sender_id?.trim() || "unknown";
|
|
56
|
+
const convType = (params.conversation_type || "direct").trim().toLowerCase();
|
|
57
|
+
if (convType === "group") {
|
|
58
|
+
return {
|
|
59
|
+
scene: "group",
|
|
60
|
+
user_id: userId,
|
|
61
|
+
group_id: params.group_id?.trim() || null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (isOwner(userId)) {
|
|
65
|
+
return { scene: "private_own", user_id: userId, group_id: null };
|
|
66
|
+
}
|
|
67
|
+
return { scene: "private_other", user_id: userId, group_id: null };
|
|
68
|
+
}
|
|
69
|
+
function resolveIdentity(sessionKey) {
|
|
70
|
+
const sk = sessionKey ?? "";
|
|
71
|
+
const sessionPart = sk.split(":").pop() || "";
|
|
72
|
+
const groupMatch = sessionPart.match(
|
|
73
|
+
/user_(.+?)_lobster_(.+?)_group_(.+?)_release_(.+)$/
|
|
74
|
+
);
|
|
75
|
+
if (groupMatch) {
|
|
76
|
+
return { scene: "group", user_id: groupMatch[1], group_id: groupMatch[3] };
|
|
77
|
+
}
|
|
78
|
+
const peerMatch = sessionPart.match(
|
|
79
|
+
/user_(.+?)_lobster_(.+?)_release_(.+)$/
|
|
80
|
+
);
|
|
81
|
+
if (peerMatch) {
|
|
82
|
+
const userId = peerMatch[1];
|
|
83
|
+
if (isOwner(userId)) {
|
|
84
|
+
return { scene: "private_own", user_id: userId, group_id: null };
|
|
85
|
+
}
|
|
86
|
+
return { scene: "private_other", user_id: userId, group_id: null };
|
|
87
|
+
}
|
|
88
|
+
return { scene: "private_other", user_id: "unknown", group_id: null };
|
|
89
|
+
}
|
|
90
|
+
function isOwner(userId) {
|
|
91
|
+
const token = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
92
|
+
if (!token) return false;
|
|
93
|
+
const ownerUserId = token.split("-").pop() || "";
|
|
94
|
+
return ownerUserId !== "" && ownerUserId === userId;
|
|
95
|
+
}
|
|
96
|
+
function extractAgentId(sessionKey) {
|
|
97
|
+
const parts = (sessionKey || "").split(":");
|
|
98
|
+
return parts.length >= 2 ? parts[1] : "main";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// index.ts
|
|
102
|
+
var buildPromptSection = ({
|
|
103
|
+
availableTools,
|
|
104
|
+
citationsMode
|
|
105
|
+
}) => {
|
|
106
|
+
const hasMemorySearch = availableTools.has("memory_search");
|
|
107
|
+
const hasMemoryGet = availableTools.has("memory_get");
|
|
108
|
+
if (!hasMemorySearch && !hasMemoryGet) return [];
|
|
109
|
+
let toolGuidance;
|
|
110
|
+
if (hasMemorySearch && hasMemoryGet) {
|
|
111
|
+
toolGuidance = "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.";
|
|
112
|
+
} else if (hasMemorySearch) {
|
|
113
|
+
toolGuidance = "Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md and answer from the matching results. If low confidence after search, say you checked.";
|
|
114
|
+
} else {
|
|
115
|
+
toolGuidance = "Before answering anything about prior work, decisions, dates, people, preferences, or todos that already point to a specific memory file or note: run memory_get to pull only the needed lines. If low confidence after reading them, say you checked.";
|
|
116
|
+
}
|
|
117
|
+
const lines = ["## Memory Recall", toolGuidance];
|
|
118
|
+
lines.push(
|
|
119
|
+
"**Context extraction:** When calling memory_search or memory_get, you MUST fill in the agent_id, sender_id, conversation_type, and group_id parameters by extracting them from the UntrustedContext section in this conversation. Look for lines like `agent_id: xxx`, `sender_id: xxx`, `conversation_type: xxx`, `group_id: xxx`. If a field is not present, omit it."
|
|
120
|
+
);
|
|
121
|
+
if (citationsMode === "off") {
|
|
122
|
+
lines.push(
|
|
123
|
+
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks."
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(
|
|
127
|
+
"Citations: include Source: <path#line> when it helps the user verify memory snippets."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
lines.push("");
|
|
131
|
+
return lines;
|
|
132
|
+
};
|
|
133
|
+
function isMemoryPath(path) {
|
|
134
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
135
|
+
return normalized === "MEMORY.md" || normalized.startsWith("memory/") || normalized.endsWith("/MEMORY.md") || normalized.includes("/memory/");
|
|
136
|
+
}
|
|
137
|
+
function formatSearchResults(results) {
|
|
138
|
+
if (results.length === 0) {
|
|
139
|
+
return "No relevant memories found.";
|
|
140
|
+
}
|
|
141
|
+
return results.map((r, i) => {
|
|
142
|
+
let sourceDesc = r.memory_type;
|
|
143
|
+
if (r.memory_type === "peer" && r.peer_id) {
|
|
144
|
+
sourceDesc = `peer/${r.peer_id}`;
|
|
145
|
+
} else if (r.memory_type === "group" && r.group_id) {
|
|
146
|
+
sourceDesc = `group/${r.group_id}`;
|
|
147
|
+
} else if (r.memory_type === "knowledge" && r.kb_id) {
|
|
148
|
+
sourceDesc = `knowledge/${r.kb_id}`;
|
|
149
|
+
}
|
|
150
|
+
const filePart = r.source_file ? ` | ${r.source_file}` : "";
|
|
151
|
+
const header = `[${i + 1}] (${sourceDesc}, score: ${r.score.toFixed(2)}${filePart})`;
|
|
152
|
+
return `${header}
|
|
153
|
+
${r.content}`;
|
|
154
|
+
}).join("\n\n");
|
|
155
|
+
}
|
|
156
|
+
var index_default = definePluginEntry({
|
|
157
|
+
id: "memory-search-plugin",
|
|
158
|
+
name: "Memory Search Plugin",
|
|
159
|
+
description: "Routes memory to external Memory Search Gateway with ACL",
|
|
160
|
+
kind: "memory",
|
|
161
|
+
register(api) {
|
|
162
|
+
const gatewayUrl = api.pluginConfig?.gatewayUrl || "http://localhost:18790";
|
|
163
|
+
const gatewayToken = api.pluginConfig?.gatewayToken || "";
|
|
164
|
+
console.log("[memory-search-plugin] gatewayUrl:", gatewayUrl);
|
|
165
|
+
const gateway = createGatewayClient({ gatewayUrl, gatewayToken });
|
|
166
|
+
api.registerMemoryPromptSection(buildPromptSection);
|
|
167
|
+
api.registerTool(
|
|
168
|
+
(ctx) => {
|
|
169
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
170
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
171
|
+
return {
|
|
172
|
+
name: "memory_search",
|
|
173
|
+
description: "Semantically search MEMORY.md and memory/**/*.md for relevant memories. You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
174
|
+
parameters: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
query: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "The search query"
|
|
180
|
+
},
|
|
181
|
+
agent_id: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
|
|
184
|
+
},
|
|
185
|
+
sender_id: {
|
|
186
|
+
type: "string",
|
|
187
|
+
description: "The sender_id from UntrustedContext"
|
|
188
|
+
},
|
|
189
|
+
conversation_type: {
|
|
190
|
+
type: "string",
|
|
191
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'"
|
|
192
|
+
},
|
|
193
|
+
group_id: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description: "The group_id from UntrustedContext (only present in group chats)"
|
|
196
|
+
},
|
|
197
|
+
maxResults: {
|
|
198
|
+
type: "number",
|
|
199
|
+
description: "Maximum number of results to return"
|
|
200
|
+
},
|
|
201
|
+
minScore: {
|
|
202
|
+
type: "number",
|
|
203
|
+
description: "Minimum similarity score threshold"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
required: ["query"]
|
|
207
|
+
},
|
|
208
|
+
async execute(toolCallId, params) {
|
|
209
|
+
const query = params.query || "";
|
|
210
|
+
if (!query) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: "No query provided." }]
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
216
|
+
const identity = params.sender_id || params.conversation_type || params.group_id ? resolveIdentityFromParams({
|
|
217
|
+
sender_id: params.sender_id,
|
|
218
|
+
conversation_type: params.conversation_type,
|
|
219
|
+
group_id: params.group_id
|
|
220
|
+
}) : fallbackIdentity || resolveIdentityFromParams({});
|
|
221
|
+
console.log(
|
|
222
|
+
`[memory-search-plugin] search: agent_id=${agentId} scene=${identity.scene} user_id=${identity.user_id} group_id=${identity.group_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
223
|
+
);
|
|
224
|
+
try {
|
|
225
|
+
const data = await gateway.callGatewaySearch({
|
|
226
|
+
agent_id: agentId,
|
|
227
|
+
user_id: identity.user_id,
|
|
228
|
+
query,
|
|
229
|
+
scene: identity.scene,
|
|
230
|
+
group_id: identity.group_id,
|
|
231
|
+
limit: params.maxResults || 20,
|
|
232
|
+
threshold: params.minScore || 0.3
|
|
233
|
+
});
|
|
234
|
+
const text = formatSearchResults(data.results);
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: "text", text }],
|
|
237
|
+
details: {
|
|
238
|
+
results: data.results,
|
|
239
|
+
total: data.total,
|
|
240
|
+
scene: data.scene,
|
|
241
|
+
steps: data.steps
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.error("[memory-search-plugin] search failed:", err.message);
|
|
246
|
+
try {
|
|
247
|
+
const fallbackTool = api.runtime?.tools?.createMemorySearchTool({
|
|
248
|
+
config: ctx.config,
|
|
249
|
+
agentSessionKey: ctx.sessionKey
|
|
250
|
+
});
|
|
251
|
+
if (fallbackTool) {
|
|
252
|
+
console.warn(
|
|
253
|
+
"[memory-search-plugin] falling back to local memory_search"
|
|
254
|
+
);
|
|
255
|
+
return await fallbackTool.execute(toolCallId, { query, maxResults: params.maxResults, minScore: params.minScore });
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: "text",
|
|
263
|
+
text: "Memory search temporarily unavailable."
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
{ names: ["memory_search"] }
|
|
272
|
+
);
|
|
273
|
+
api.registerTool(
|
|
274
|
+
(ctx) => {
|
|
275
|
+
const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
|
|
276
|
+
const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
|
|
277
|
+
return {
|
|
278
|
+
name: "memory_get",
|
|
279
|
+
description: "Read a memory file or workspace file. Supports reading MEMORY.md, memory/**/*.md, and other workspace files. You MUST extract agent_id, sender_id, conversation_type, group_id from the UntrustedContext in this conversation and pass them as parameters.",
|
|
280
|
+
parameters: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
path: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)"
|
|
286
|
+
},
|
|
287
|
+
agent_id: {
|
|
288
|
+
type: "string",
|
|
289
|
+
description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
|
|
290
|
+
},
|
|
291
|
+
sender_id: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "The sender_id from UntrustedContext"
|
|
294
|
+
},
|
|
295
|
+
conversation_type: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "The conversation_type from UntrustedContext: 'direct' or 'group'"
|
|
298
|
+
},
|
|
299
|
+
group_id: {
|
|
300
|
+
type: "string",
|
|
301
|
+
description: "The group_id from UntrustedContext (only present in group chats)"
|
|
302
|
+
},
|
|
303
|
+
from: {
|
|
304
|
+
type: "number",
|
|
305
|
+
description: "Starting line number (1-indexed)"
|
|
306
|
+
},
|
|
307
|
+
lines: {
|
|
308
|
+
type: "number",
|
|
309
|
+
description: "Number of lines to read"
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
required: ["path"]
|
|
313
|
+
},
|
|
314
|
+
async execute(toolCallId, params) {
|
|
315
|
+
const path = params.path || "";
|
|
316
|
+
if (!path) {
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: "No path provided." }]
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
let originalGetTool = null;
|
|
322
|
+
try {
|
|
323
|
+
originalGetTool = api.runtime?.tools?.createMemoryGetTool({
|
|
324
|
+
config: ctx.config,
|
|
325
|
+
agentSessionKey: ctx.sessionKey
|
|
326
|
+
});
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
if (isMemoryPath(path)) {
|
|
330
|
+
const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
|
|
331
|
+
const identity = params.sender_id || params.conversation_type || params.group_id ? resolveIdentityFromParams({
|
|
332
|
+
sender_id: params.sender_id,
|
|
333
|
+
conversation_type: params.conversation_type,
|
|
334
|
+
group_id: params.group_id
|
|
335
|
+
}) : fallbackIdentity || resolveIdentityFromParams({});
|
|
336
|
+
console.log(
|
|
337
|
+
`[memory-search-plugin] get: path=${path} agent_id=${agentId} scene=${identity.scene} user_id=${identity.user_id} (source=${params.sender_id ? "llm_params" : "fallback"})`
|
|
338
|
+
);
|
|
339
|
+
try {
|
|
340
|
+
const data = await gateway.callGatewayGet({
|
|
341
|
+
agent_id: agentId,
|
|
342
|
+
user_id: identity.user_id,
|
|
343
|
+
path,
|
|
344
|
+
scene: identity.scene,
|
|
345
|
+
group_id: identity.group_id,
|
|
346
|
+
from: params.from,
|
|
347
|
+
lines: params.lines
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text", text: data.text }],
|
|
351
|
+
details: { path: data.path, text: data.text }
|
|
352
|
+
};
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.error("[memory-search-plugin] get failed:", err.message);
|
|
355
|
+
if (err.message?.includes("403")) {
|
|
356
|
+
return {
|
|
357
|
+
content: [
|
|
358
|
+
{
|
|
359
|
+
type: "text",
|
|
360
|
+
text: `Access denied: ${path}`
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (originalGetTool) {
|
|
366
|
+
console.warn(
|
|
367
|
+
"[memory-search-plugin] falling back to local memory_get for:",
|
|
368
|
+
path
|
|
369
|
+
);
|
|
370
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: `Failed to read ${path}: Gateway unavailable.`
|
|
377
|
+
}
|
|
378
|
+
],
|
|
379
|
+
details: { path, text: "", error: err.message }
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
if (originalGetTool) {
|
|
384
|
+
return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: `Cannot read ${path}: memory_get backend not available.`
|
|
391
|
+
}
|
|
392
|
+
]
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
},
|
|
398
|
+
{ names: ["memory_get"] }
|
|
399
|
+
);
|
|
400
|
+
api.registerCli(
|
|
401
|
+
({ program }) => {
|
|
402
|
+
try {
|
|
403
|
+
api.runtime?.tools?.registerMemoryCli(program);
|
|
404
|
+
} catch {
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{ commands: ["memory"] }
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
export {
|
|
412
|
+
index_default as default
|
|
413
|
+
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Memory Search Plugin",
|
|
4
4
|
"description": "Routes memory_search to external Memory Search Gateway with ACL, memory_get with path routing",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memory-search-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "index.
|
|
5
|
+
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
|
+
"*.js",
|
|
7
8
|
"*.ts",
|
|
8
9
|
"openclaw.plugin.json",
|
|
9
10
|
"!*.test.ts"
|
|
@@ -14,7 +15,7 @@
|
|
|
14
15
|
},
|
|
15
16
|
"openclaw": {
|
|
16
17
|
"extensions": [
|
|
17
|
-
"./index.
|
|
18
|
+
"./index.js"
|
|
18
19
|
]
|
|
19
20
|
}
|
|
20
21
|
}
|