memory-search-plugin 0.8.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,190 +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
- console.log("[memory-search-plugin] gatewayUrl:", gatewayUrl);
165
- const gateway = createGatewayClient({ gatewayUrl, gatewayToken });
166
- 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
+ });
167
88
  api.registerTool(
168
89
  (ctx) => {
169
- const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
170
- const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
171
90
  return {
172
91
  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.",
92
+ description: "Semantically search memories. Extract owner_id, sender_id, agent_id, conversation_type, group_id from UntrustedContext.",
174
93
  parameters: {
175
94
  type: "object",
176
95
  properties: {
177
- query: {
96
+ query: { type: "string", description: "The search query" },
97
+ owner_id: {
178
98
  type: "string",
179
- description: "The search query"
99
+ description: "The owner_id from UntrustedContext (agent owner's user_id, always present)"
180
100
  },
181
- agent_id: {
101
+ sender_id: {
182
102
  type: "string",
183
- description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
103
+ description: "The sender_id from UntrustedContext (current message sender)"
184
104
  },
185
- sender_id: {
105
+ agent_id: {
186
106
  type: "string",
187
- description: "The sender_id from UntrustedContext"
107
+ description: "The agent_id from UntrustedContext"
188
108
  },
189
109
  conversation_type: {
190
110
  type: "string",
@@ -192,15 +112,15 @@ var index_default = definePluginEntry({
192
112
  },
193
113
  group_id: {
194
114
  type: "string",
195
- description: "The group_id from UntrustedContext (only present in group chats)"
115
+ description: "The group_id from UntrustedContext (only in group chats)"
196
116
  },
197
117
  maxResults: {
198
118
  type: "number",
199
- description: "Maximum number of results to return"
119
+ description: "Max results (default 20)"
200
120
  },
201
121
  minScore: {
202
122
  type: "number",
203
- description: "Minimum similarity score threshold"
123
+ description: "Min similarity score 0-1 (default 0.3)"
204
124
  }
205
125
  },
206
126
  required: ["query"]
@@ -212,50 +132,35 @@ var index_default = definePluginEntry({
212
132
  content: [{ type: "text", text: "No query provided." }]
213
133
  };
214
134
  }
215
- const agentId = params.agent_id?.trim() || fallbackAgentId || "main";
216
- const identity = params.sender_id || params.conversation_type || params.group_id ? resolveIdentityFromParams({
135
+ const identity = resolveIdentity({
217
136
  sender_id: params.sender_id,
137
+ owner_id: params.owner_id,
218
138
  conversation_type: params.conversation_type,
219
139
  group_id: params.group_id
220
- }) : fallbackIdentity || resolveIdentityFromParams({});
140
+ });
141
+ const agentId = params.agent_id?.trim() || "main";
221
142
  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"})`
143
+ `[memory-search] search: agent=${agentId} scene=${identity.scene} sender=${identity.sender_id} owner=${identity.owner_id} group=${identity.group_id}`
223
144
  );
224
145
  try {
225
146
  const data = await gateway.callGatewaySearch({
147
+ owner_id: identity.owner_id,
148
+ sender_id: identity.sender_id,
226
149
  agent_id: agentId,
227
- user_id: identity.user_id,
228
150
  query,
229
151
  scene: identity.scene,
230
152
  group_id: identity.group_id,
231
153
  limit: params.maxResults || 20,
232
154
  threshold: params.minScore || 0.3
233
155
  });
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
- };
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 }] };
244
162
  } 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
- }
163
+ console.error("[memory-search] search failed:", err.message);
259
164
  return {
260
165
  content: [
261
166
  {
@@ -272,122 +177,89 @@ var index_default = definePluginEntry({
272
177
  );
273
178
  api.registerTool(
274
179
  (ctx) => {
275
- const fallbackIdentity = ctx.sessionKey ? resolveIdentity(ctx.sessionKey) : null;
276
- const fallbackAgentId = ctx.sessionKey ? extractAgentId(ctx.sessionKey) : null;
277
180
  return {
278
181
  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.",
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.",
280
183
  parameters: {
281
184
  type: "object",
282
185
  properties: {
283
- path: {
186
+ keyword: {
284
187
  type: "string",
285
- description: "Relative path to the file (e.g. MEMORY.md, memory/notes.md, SOUL.md)"
188
+ description: "Keyword to search in chat messages"
286
189
  },
287
- agent_id: {
190
+ owner_id: {
288
191
  type: "string",
289
- description: "The agent_id from UntrustedContext (e.g. 'main' or 'lobster_xxx')"
192
+ description: "The owner_id from UntrustedContext"
290
193
  },
291
194
  sender_id: {
292
195
  type: "string",
293
196
  description: "The sender_id from UntrustedContext"
294
197
  },
198
+ agent_id: {
199
+ type: "string",
200
+ description: "The agent_id from UntrustedContext"
201
+ },
295
202
  conversation_type: {
296
203
  type: "string",
297
204
  description: "The conversation_type from UntrustedContext: 'direct' or 'group'"
298
205
  },
299
206
  group_id: {
300
207
  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)"
208
+ description: "The group_id from UntrustedContext (only in group chats)"
306
209
  },
307
- lines: {
210
+ limit: {
308
211
  type: "number",
309
- description: "Number of lines to read"
212
+ description: "Max messages to return (default 20)"
310
213
  }
311
214
  },
312
- required: ["path"]
215
+ required: ["keyword"]
313
216
  },
314
217
  async execute(toolCallId, params) {
315
- const path = params.path || "";
316
- if (!path) {
218
+ const keyword = params.keyword || "";
219
+ if (!keyword) {
317
220
  return {
318
- content: [{ type: "text", text: "No path provided." }]
221
+ content: [{ type: "text", text: "No keyword provided." }]
319
222
  };
320
223
  }
321
- 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
+ );
322
234
  try {
323
- originalGetTool = api.runtime?.tools?.createMemoryGetTool({
324
- config: ctx.config,
325
- 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
326
243
  });
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
- }
244
+ if (data.messages.length === 0) {
372
245
  return {
373
246
  content: [
374
- {
375
- type: "text",
376
- text: `Failed to read ${path}: Gateway unavailable.`
377
- }
378
- ],
379
- details: { path, text: "", error: err.message }
247
+ { type: "text", text: "No matching messages found." }
248
+ ]
380
249
  };
381
250
  }
382
- } else {
383
- if (originalGetTool) {
384
- return await originalGetTool.execute(toolCallId, { path, from: params.from, lines: params.lines });
385
- }
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);
386
258
  return {
387
259
  content: [
388
260
  {
389
261
  type: "text",
390
- text: `Cannot read ${path}: memory_get backend not available.`
262
+ text: "Message search temporarily unavailable."
391
263
  }
392
264
  ]
393
265
  };
@@ -397,17 +269,8 @@ var index_default = definePluginEntry({
397
269
  },
398
270
  { names: ["memory_get"] }
399
271
  );
400
- api.registerCli(
401
- ({ program }) => {
402
- try {
403
- api.runtime?.tools?.registerMemoryCli(program);
404
- } catch {
405
- }
406
- },
407
- { commands: ["memory"] }
408
- );
409
272
  }
410
- });
273
+ };
411
274
  export {
412
275
  index_default as default
413
276
  };