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