memory-search-plugin 0.10.0 → 1.1.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,192 +1,110 @@
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
1
  // identity.ts
44
- function resolveIdentityFromParams(params) {
2
+ function resolveIdentity(params) {
45
3
  const testScene = process.env.MEMORY_GATEWAY_TEST_SCENE;
46
4
  if (testScene) {
47
- const result = {
5
+ return {
48
6
  scene: testScene,
49
- user_id: process.env.MEMORY_GATEWAY_TEST_USER_ID || "owner_A",
7
+ sender_id: process.env.MEMORY_GATEWAY_TEST_USER_ID || "test_user",
8
+ owner_id: process.env.MEMORY_GATEWAY_TEST_OWNER_ID || "test_owner",
50
9
  group_id: process.env.MEMORY_GATEWAY_TEST_GROUP_ID || null
51
10
  };
52
- console.log("[memory-search-plugin] TEST MODE identity:", JSON.stringify(result));
53
- return result;
54
11
  }
55
- const userId = params.sender_id?.trim() || "unknown";
12
+ const senderId = params.sender_id?.trim() || "unknown";
13
+ const ownerId = params.owner_id?.trim() || "";
56
14
  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
- };
15
+ const groupId = params.group_id?.trim() || null;
16
+ if (!ownerId) {
17
+ console.warn(
18
+ "[identity] owner_id is empty \u2014 LLM failed to extract from UntrustedContext. owner queries will be skipped to prevent cross-user leak."
19
+ );
63
20
  }
64
- if (isOwner(userId)) {
65
- return { scene: "private_own", user_id: userId, group_id: null };
21
+ if (convType === "group") {
22
+ if (!groupId) {
23
+ console.warn("[identity] group scene but group_id is empty");
24
+ }
25
+ return { scene: "group", sender_id: senderId, owner_id: ownerId, group_id: groupId };
66
26
  }
67
- return { scene: "private_other", user_id: userId, group_id: null };
27
+ return { scene: "owner", sender_id: senderId, owner_id: ownerId, group_id: null };
68
28
  }
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 };
29
+
30
+ // gateway-client.ts
31
+ function createGatewayClient(config) {
32
+ const baseUrl = config.gatewayUrl.replace(/\/+$/, "");
33
+ const headers = {
34
+ "Content-Type": "application/json",
35
+ ...config.gatewayToken ? { Authorization: `Bearer ${config.gatewayToken}` } : {}
36
+ };
37
+ async function post(path, body) {
38
+ const resp = await fetch(`${baseUrl}${path}`, {
39
+ method: "POST",
40
+ headers,
41
+ body: JSON.stringify(body)
42
+ });
43
+ if (!resp.ok) {
44
+ const text = await resp.text().catch(() => "");
45
+ throw new Error(`Gateway ${path} failed: ${resp.status} ${text}`);
85
46
  }
86
- return { scene: "private_other", user_id: userId, group_id: null };
47
+ return resp.json();
87
48
  }
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";
49
+ return {
50
+ callGatewaySearch: (opts) => post("/api/memory/search", opts),
51
+ callGatewayGet: (opts) => post("/api/memory/get", opts)
52
+ };
99
53
  }
100
54
 
101
55
  // index.ts
102
56
  var buildPromptSection = ({
103
- availableTools,
104
- citationsMode
57
+ availableTools
105
58
  }) => {
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("");
59
+ const hasSearch = availableTools.has("memory_search");
60
+ const hasGet = availableTools.has("memory_get");
61
+ if (!hasSearch && !hasGet) return [];
62
+ const lines = [
63
+ "## Memory Recall",
64
+ "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.",
65
+ "",
66
+ "**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 \u2014 only use what is explicitly stated in UntrustedContext.",
67
+ ""
68
+ ];
131
69
  return lines;
132
70
  };
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({
71
+ var index_default = {
157
72
  id: "memory-search-plugin",
158
- name: "Memory Search Plugin",
159
- description: "Routes memory to external Memory Search Gateway with ACL",
160
- kind: "memory",
161
73
  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 });
168
- api.registerMemoryPromptSection(buildPromptSection);
74
+ const config = {
75
+ gatewayUrl: api.getConfig?.("MEMORY_GATEWAY_URL") || process.env.MEMORY_GATEWAY_URL || "",
76
+ gatewayToken: api.getConfig?.("MEMORY_GATEWAY_TOKEN") || process.env.MEMORY_GATEWAY_TOKEN || ""
77
+ };
78
+ if (!config.gatewayUrl) {
79
+ console.warn("[memory-search-plugin] MEMORY_GATEWAY_URL not set, plugin disabled");
80
+ return;
81
+ }
82
+ const gateway = createGatewayClient(config);
83
+ api.registerPromptSection?.({
84
+ id: "memory-recall",
85
+ position: "top",
86
+ build: buildPromptSection
87
+ });
169
88
  api.registerTool(
170
89
  (ctx) => {
171
- const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
172
- const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
173
90
  return {
174
91
  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.",
92
+ description: "Semantically search memories. Extract owner_id, sender_id, agent_id, conversation_type, group_id from UntrustedContext.",
176
93
  parameters: {
177
94
  type: "object",
178
95
  properties: {
179
- query: {
96
+ query: { type: "string", description: "The search query" },
97
+ owner_id: {
180
98
  type: "string",
181
- description: "The search query"
99
+ description: "The owner_id from UntrustedContext (agent owner's user_id, always present)"
182
100
  },
183
- agent_id: {
101
+ sender_id: {
184
102
  type: "string",
185
- description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
103
+ description: "The sender_id from UntrustedContext (current message sender)"
186
104
  },
187
- sender_id: {
105
+ agent_id: {
188
106
  type: "string",
189
- description: "The sender_id from UntrustedContext"
107
+ description: "The agent_id from UntrustedContext"
190
108
  },
191
109
  conversation_type: {
192
110
  type: "string",
@@ -194,15 +112,15 @@ var index_default = definePluginEntry({
194
112
  },
195
113
  group_id: {
196
114
  type: "string",
197
- description: "The group_id from UntrustedContext (only present in group chats)"
115
+ description: "The group_id from UntrustedContext (only in group chats)"
198
116
  },
199
117
  maxResults: {
200
118
  type: "number",
201
- description: "Maximum number of results to return"
119
+ description: "Max results (default 20)"
202
120
  },
203
121
  minScore: {
204
122
  type: "number",
205
- description: "Minimum similarity score threshold"
123
+ description: "Min similarity score 0-1 (default 0.3)"
206
124
  }
207
125
  },
208
126
  required: ["query"]
@@ -214,51 +132,35 @@ var index_default = definePluginEntry({
214
132
  content: [{ type: "text", text: "No query provided." }]
215
133
  };
216
134
  }
217
- const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
218
- const identity = params.sender_id || params.conversation_type || params.group_id ? resolveIdentityFromParams({
135
+ const identity = resolveIdentity({
219
136
  sender_id: params.sender_id,
137
+ owner_id: params.owner_id,
220
138
  conversation_type: params.conversation_type,
221
139
  group_id: params.group_id
222
- }) : fallbackIdentity || resolveIdentityFromParams({});
140
+ });
141
+ const agentId = params.agent_id?.trim() || "main";
223
142
  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"})`
143
+ `[memory-search] search: agent=${agentId} scene=${identity.scene} sender=${identity.sender_id} owner=${identity.owner_id} group=${identity.group_id}`
225
144
  );
226
145
  try {
227
146
  const data = await gateway.callGatewaySearch({
147
+ owner_id: identity.owner_id,
148
+ sender_id: identity.sender_id,
228
149
  agent_id: agentId,
229
- user_id: identity.user_id,
230
150
  query,
231
151
  scene: identity.scene,
232
152
  group_id: identity.group_id,
233
- release_name: releaseName || undefined,
234
153
  limit: params.maxResults || 20,
235
154
  threshold: params.minScore || 0.3
236
155
  });
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
- };
156
+ const text = data.results.length === 0 ? "No relevant memories found." : data.results.map((r, i) => {
157
+ 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;
158
+ return `[${i + 1}] (${tag}, score: ${r.score.toFixed(2)})
159
+ ${r.content}`;
160
+ }).join("\n\n");
161
+ return { content: [{ type: "text", text }] };
247
162
  } 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
- }
163
+ console.error("[memory-search] search failed:", err.message);
262
164
  return {
263
165
  content: [
264
166
  {
@@ -275,123 +177,89 @@ var index_default = definePluginEntry({
275
177
  );
276
178
  api.registerTool(
277
179
  (ctx) => {
278
- const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
279
- const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
280
180
  return {
281
181
  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.",
182
+ 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
183
  parameters: {
284
184
  type: "object",
285
185
  properties: {
286
- path: {
186
+ keyword: {
287
187
  type: "string",
288
- description: "Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)"
188
+ description: "Keyword to search in chat messages"
289
189
  },
290
- agent_id: {
190
+ owner_id: {
291
191
  type: "string",
292
- description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
192
+ description: "The owner_id from UntrustedContext"
293
193
  },
294
194
  sender_id: {
295
195
  type: "string",
296
196
  description: "The sender_id from UntrustedContext"
297
197
  },
198
+ agent_id: {
199
+ type: "string",
200
+ description: "The agent_id from UntrustedContext"
201
+ },
298
202
  conversation_type: {
299
203
  type: "string",
300
204
  description: "The conversation_type from UntrustedContext: 'direct' or 'group'"
301
205
  },
302
206
  group_id: {
303
207
  type: "string",
304
- description: "The group_id from UntrustedContext (only present in group chats)"
305
- },
306
- from: {
307
- type: "number",
308
- description: "Starting line number (1-indexed)"
208
+ description: "The group_id from UntrustedContext (only in group chats)"
309
209
  },
310
- lines: {
210
+ limit: {
311
211
  type: "number",
312
- description: "Number of lines to read"
212
+ description: "Max messages to return (default 20)"
313
213
  }
314
214
  },
315
- required: ["path"]
215
+ required: ["keyword"]
316
216
  },
317
217
  async execute(toolCallId, params) {
318
- const path = params.path || "";
319
- if (!path) {
218
+ const keyword = params.keyword || "";
219
+ if (!keyword) {
320
220
  return {
321
- content: [{ type: "text", text: "No path provided." }]
221
+ content: [{ type: "text", text: "No keyword provided." }]
322
222
  };
323
223
  }
324
- let originalGetTool = null;
224
+ const identity = resolveIdentity({
225
+ sender_id: params.sender_id,
226
+ owner_id: params.owner_id,
227
+ conversation_type: params.conversation_type,
228
+ group_id: params.group_id
229
+ });
230
+ const agentId = params.agent_id?.trim() || "main";
231
+ console.log(
232
+ `[memory-search] get: keyword="${keyword}" agent=${agentId} scene=${identity.scene} owner=${identity.owner_id} group=${identity.group_id}`
233
+ );
325
234
  try {
326
- originalGetTool = api.runtime?.tools?.createMemoryGetTool({
327
- config: ctx.config,
328
- agentSessionKey: ctx.sessionKey
235
+ const data = await gateway.callGatewayGet({
236
+ owner_id: identity.owner_id,
237
+ sender_id: identity.sender_id,
238
+ agent_id: agentId,
239
+ keyword,
240
+ scene: identity.scene,
241
+ group_id: identity.group_id,
242
+ limit: params.limit || 20
329
243
  });
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
- }
244
+ if (data.messages.length === 0) {
376
245
  return {
377
246
  content: [
378
- {
379
- type: "text",
380
- text: `Failed to read ${path}: Gateway unavailable.`
381
- }
382
- ],
383
- details: { path, text: "", error: err.message }
247
+ { type: "text", text: "No matching messages found." }
248
+ ]
384
249
  };
385
250
  }
386
- } else {
387
- if (originalGetTool) {
388
- return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
389
- }
251
+ const text = data.messages.map((m) => {
252
+ const tag = m.message_type === "group" && m.group_id ? `group/${m.group_id}` : m.message_type;
253
+ return `[${tag}] ${m.formatted_text}`;
254
+ }).join("\n");
255
+ return { content: [{ type: "text", text }] };
256
+ } catch (err) {
257
+ console.error("[memory-search] get failed:", err.message);
390
258
  return {
391
259
  content: [
392
260
  {
393
261
  type: "text",
394
- text: `Cannot read ${path}: memory_get backend not available.`
262
+ text: "Message search temporarily unavailable."
395
263
  }
396
264
  ]
397
265
  };
@@ -401,17 +269,8 @@ var index_default = definePluginEntry({
401
269
  },
402
270
  { names: ["memory_get"] }
403
271
  );
404
- api.registerCli(
405
- ({ program }) => {
406
- try {
407
- api.runtime?.tools?.registerMemoryCli(program);
408
- } catch {
409
- }
410
- },
411
- { commands: ["memory"] }
412
- );
413
272
  }
414
- });
273
+ };
415
274
  export {
416
275
  index_default as default
417
276
  };