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 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
+ };
@@ -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.1.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.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
- "main": "index.ts",
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.ts"
18
+ "./index.js"
18
19
  ]
19
20
  }
20
21
  }