@memtensor/memos-cloud-openclaw-plugin 0.1.5-beta.0 → 0.1.5

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,259 +1,259 @@
1
- #!/usr/bin/env node
2
- import {
3
- addMessage,
4
- buildConfig,
5
- extractText,
6
- formatPromptBlock,
7
- USER_QUERY_MARKER,
8
- searchMemory,
9
- } from "./lib/memos-cloud-api.js";
10
- let lastCaptureTime = 0;
11
- const conversationCounters = new Map();
12
- const API_KEY_HELP_URL = "https://memos-dashboard.openmem.net/cn/apikeys/";
13
- const ENV_FILE_SEARCH_HINTS = ["~/.openclaw/.env", "~/.moltbot/.env", "~/.clawdbot/.env"];
14
- const MEMOS_SOURCE = "openclaw";
15
-
16
- function warnMissingApiKey(log, context) {
17
- const heading = "[memos-cloud] Missing MEMOS_API_KEY (Token auth)";
18
- const header = `${heading}${context ? `; ${context} skipped` : ""}. Configure it with:`;
19
- log.warn?.(
20
- [
21
- header,
22
- "echo 'export MEMOS_API_KEY=\"mpg-...\"' >> ~/.zshrc",
23
- "source ~/.zshrc",
24
- "or",
25
- "echo 'export MEMOS_API_KEY=\"mpg-...\"' >> ~/.bashrc",
26
- "source ~/.bashrc",
27
- "or",
28
- "[System.Environment]::SetEnvironmentVariable(\"MEMOS_API_KEY\", \"mpg-...\", \"User\")",
29
- `Get API key: ${API_KEY_HELP_URL}`,
30
- ].join("\n"),
31
- );
32
- }
33
-
34
- function stripPrependedPrompt(content) {
35
- if (!content) return content;
36
- const idx = content.lastIndexOf(USER_QUERY_MARKER);
37
- if (idx === -1) return content;
38
- return content.slice(idx + USER_QUERY_MARKER.length).trimStart();
39
- }
40
-
41
- function getCounterSuffix(sessionKey) {
42
- if (!sessionKey) return "";
43
- const current = conversationCounters.get(sessionKey) ?? 0;
44
- return current > 0 ? `#${current}` : "";
45
- }
46
-
47
- function bumpConversationCounter(sessionKey) {
48
- if (!sessionKey) return;
49
- const current = conversationCounters.get(sessionKey) ?? 0;
50
- conversationCounters.set(sessionKey, current + 1);
51
- }
52
-
53
- function resolveConversationId(cfg, ctx) {
54
- if (cfg.conversationId) return cfg.conversationId;
55
- // TODO: consider binding conversation_id directly to OpenClaw sessionId (prefer ctx.sessionId).
56
- const base = ctx?.sessionKey || ctx?.sessionId || (ctx?.agentId ? `openclaw:${ctx.agentId}` : "");
57
- const dynamicSuffix = cfg.conversationSuffixMode === "counter" ? getCounterSuffix(ctx?.sessionKey) : "";
58
- const prefix = cfg.conversationIdPrefix || "";
59
- const suffix = cfg.conversationIdSuffix || "";
60
- if (base) return `${prefix}${base}${dynamicSuffix}${suffix}`;
61
- return `${prefix}openclaw-${Date.now()}${dynamicSuffix}${suffix}`;
62
- }
63
-
64
- function buildSearchPayload(cfg, prompt, ctx) {
65
- const queryRaw = `${cfg.queryPrefix || ""}${prompt}`;
66
- const query =
67
- Number.isFinite(cfg.maxQueryChars) && cfg.maxQueryChars > 0
68
- ? queryRaw.slice(0, cfg.maxQueryChars)
69
- : queryRaw;
70
-
71
- const payload = {
72
- user_id: cfg.userId,
73
- query,
74
- source: MEMOS_SOURCE,
75
- };
76
-
77
- if (!cfg.recallGlobal) {
78
- const conversationId = resolveConversationId(cfg, ctx);
79
- if (conversationId) payload.conversation_id = conversationId;
80
- }
81
-
82
- if (cfg.filter) payload.filter = cfg.filter;
83
- if (cfg.knowledgebaseIds?.length) payload.knowledgebase_ids = cfg.knowledgebaseIds;
84
-
85
- payload.memory_limit_number = cfg.memoryLimitNumber;
86
- payload.include_preference = cfg.includePreference;
87
- payload.preference_limit_number = cfg.preferenceLimitNumber;
88
- payload.include_tool_memory = cfg.includeToolMemory;
89
- payload.tool_memory_limit_number = cfg.toolMemoryLimitNumber;
90
- payload.relativity = cfg.relativity;
91
-
92
- return payload;
93
- }
94
-
95
- function buildAddMessagePayload(cfg, messages, ctx) {
96
- const payload = {
97
- user_id: cfg.userId,
98
- conversation_id: resolveConversationId(cfg, ctx),
99
- messages,
100
- source: MEMOS_SOURCE,
101
- };
102
-
103
- if (cfg.agentId) payload.agent_id = cfg.agentId;
104
- if (cfg.appId) payload.app_id = cfg.appId;
105
- if (cfg.tags?.length) payload.tags = cfg.tags;
106
-
107
- const info = {
108
- source: "openclaw",
109
- sessionKey: ctx?.sessionKey,
110
- agentId: ctx?.agentId,
111
- ...(cfg.info || {}),
112
- };
113
- if (Object.keys(info).length > 0) payload.info = info;
114
-
115
- payload.allow_public = cfg.allowPublic;
116
- if (cfg.allowKnowledgebaseIds?.length) payload.allow_knowledgebase_ids = cfg.allowKnowledgebaseIds;
117
- payload.async_mode = cfg.asyncMode;
118
-
119
- return payload;
120
- }
121
-
122
- function pickLastTurnMessages(messages, cfg) {
123
- const lastUserIndex = messages
124
- .map((m, idx) => ({ m, idx }))
125
- .filter(({ m }) => m?.role === "user")
126
- .map(({ idx }) => idx)
127
- .pop();
128
-
129
- if (lastUserIndex === undefined) return [];
130
-
131
- const slice = messages.slice(lastUserIndex);
132
- const results = [];
133
-
134
- for (const msg of slice) {
135
- if (!msg || !msg.role) continue;
136
- if (msg.role === "user") {
137
- const content = stripPrependedPrompt(extractText(msg.content));
138
- if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
139
- continue;
140
- }
141
- if (msg.role === "assistant" && cfg.includeAssistant) {
142
- const content = extractText(msg.content);
143
- if (content) results.push({ role: "assistant", content: truncate(content, cfg.maxMessageChars) });
144
- }
145
- }
146
-
147
- return results;
148
- }
149
-
150
- function pickFullSessionMessages(messages, cfg) {
151
- const results = [];
152
- for (const msg of messages) {
153
- if (!msg || !msg.role) continue;
154
- if (msg.role === "user") {
155
- const content = stripPrependedPrompt(extractText(msg.content));
156
- if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
157
- }
158
- if (msg.role === "assistant" && cfg.includeAssistant) {
159
- const content = extractText(msg.content);
160
- if (content) results.push({ role: "assistant", content: truncate(content, cfg.maxMessageChars) });
161
- }
162
- }
163
- return results;
164
- }
165
-
166
- function truncate(text, maxLen) {
167
- if (!text) return "";
168
- if (!maxLen) return text;
169
- return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
170
- }
171
-
172
- export default {
173
- id: "memos-cloud-openclaw-plugin",
174
- name: "MemOS Cloud OpenClaw Plugin",
175
- description: "MemOS Cloud recall + add memory via lifecycle hooks",
176
- kind: "lifecycle",
177
-
178
- register(api) {
179
- const cfg = buildConfig(api.pluginConfig);
180
- const log = api.logger ?? console;
181
-
182
- if (!cfg.envFileStatus?.found) {
183
- const searchPaths = cfg.envFileStatus?.searchPaths?.join(", ") ?? ENV_FILE_SEARCH_HINTS.join(", ");
184
- log.warn?.(`[memos-cloud] No .env found in ${searchPaths}; falling back to process env or plugin config.`);
185
- }
186
-
187
- if (cfg.conversationSuffixMode === "counter" && cfg.resetOnNew) {
188
- if (api.config?.hooks?.internal?.enabled !== true) {
189
- log.warn?.("[memos-cloud] command:new hook requires hooks.internal.enabled = true");
190
- }
191
- api.registerHook(
192
- ["command:new"],
193
- (event) => {
194
- if (event?.type === "command" && event?.action === "new") {
195
- bumpConversationCounter(event.sessionKey);
196
- }
197
- },
198
- {
199
- name: "memos-cloud-conversation-new",
200
- description: "Increment MemOS conversation suffix on /new",
201
- },
202
- );
203
- }
204
-
205
- api.on("before_agent_start", async (event, ctx) => {
206
- if (!cfg.recallEnabled) return;
207
- if (!event?.prompt || event.prompt.length < 3) return;
208
- if (!cfg.apiKey) {
209
- warnMissingApiKey(log, "recall");
210
- return;
211
- }
212
-
213
- try {
214
- const payload = buildSearchPayload(cfg, event.prompt, ctx);
215
- const result = await searchMemory(cfg, payload);
216
- const promptBlock = formatPromptBlock(result, {
217
- wrapTagBlocks: true,
218
- relativity: payload.relativity
219
- });
220
- if (!promptBlock) return;
221
-
222
- return {
223
- prependContext: promptBlock,
224
- };
225
- } catch (err) {
226
- log.warn?.(`[memos-cloud] recall failed: ${String(err)}`);
227
- }
228
- });
229
-
230
- api.on("agent_end", async (event, ctx) => {
231
- if (!cfg.addEnabled) return;
232
- if (!event?.success || !event?.messages?.length) return;
233
- if (!cfg.apiKey) {
234
- warnMissingApiKey(log, "add");
235
- return;
236
- }
237
-
238
- const now = Date.now();
239
- if (cfg.throttleMs && now - lastCaptureTime < cfg.throttleMs) {
240
- return;
241
- }
242
- lastCaptureTime = now;
243
-
244
- try {
245
- const messages =
246
- cfg.captureStrategy === "full_session"
247
- ? pickFullSessionMessages(event.messages, cfg)
248
- : pickLastTurnMessages(event.messages, cfg);
249
-
250
- if (!messages.length) return;
251
-
252
- const payload = buildAddMessagePayload(cfg, messages, ctx);
253
- await addMessage(cfg, payload);
254
- } catch (err) {
255
- log.warn?.(`[memos-cloud] add failed: ${String(err)}`);
256
- }
257
- });
258
- },
259
- };
1
+ #!/usr/bin/env node
2
+ import {
3
+ addMessage,
4
+ buildConfig,
5
+ extractText,
6
+ formatPromptBlock,
7
+ USER_QUERY_MARKER,
8
+ searchMemory,
9
+ } from "./lib/memos-cloud-api.js";
10
+ let lastCaptureTime = 0;
11
+ const conversationCounters = new Map();
12
+ const API_KEY_HELP_URL = "https://memos-dashboard.openmem.net/cn/apikeys/";
13
+ const ENV_FILE_SEARCH_HINTS = ["~/.openclaw/.env", "~/.moltbot/.env", "~/.clawdbot/.env"];
14
+ const MEMOS_SOURCE = "openclaw";
15
+
16
+ function warnMissingApiKey(log, context) {
17
+ const heading = "[memos-cloud] Missing MEMOS_API_KEY (Token auth)";
18
+ const header = `${heading}${context ? `; ${context} skipped` : ""}. Configure it with:`;
19
+ log.warn?.(
20
+ [
21
+ header,
22
+ "echo 'export MEMOS_API_KEY=\"mpg-...\"' >> ~/.zshrc",
23
+ "source ~/.zshrc",
24
+ "or",
25
+ "echo 'export MEMOS_API_KEY=\"mpg-...\"' >> ~/.bashrc",
26
+ "source ~/.bashrc",
27
+ "or",
28
+ "[System.Environment]::SetEnvironmentVariable(\"MEMOS_API_KEY\", \"mpg-...\", \"User\")",
29
+ `Get API key: ${API_KEY_HELP_URL}`,
30
+ ].join("\n"),
31
+ );
32
+ }
33
+
34
+ function stripPrependedPrompt(content) {
35
+ if (!content) return content;
36
+ const idx = content.lastIndexOf(USER_QUERY_MARKER);
37
+ if (idx === -1) return content;
38
+ return content.slice(idx + USER_QUERY_MARKER.length).trimStart();
39
+ }
40
+
41
+ function getCounterSuffix(sessionKey) {
42
+ if (!sessionKey) return "";
43
+ const current = conversationCounters.get(sessionKey) ?? 0;
44
+ return current > 0 ? `#${current}` : "";
45
+ }
46
+
47
+ function bumpConversationCounter(sessionKey) {
48
+ if (!sessionKey) return;
49
+ const current = conversationCounters.get(sessionKey) ?? 0;
50
+ conversationCounters.set(sessionKey, current + 1);
51
+ }
52
+
53
+ function resolveConversationId(cfg, ctx) {
54
+ if (cfg.conversationId) return cfg.conversationId;
55
+ // TODO: consider binding conversation_id directly to OpenClaw sessionId (prefer ctx.sessionId).
56
+ const base = ctx?.sessionKey || ctx?.sessionId || (ctx?.agentId ? `openclaw:${ctx.agentId}` : "");
57
+ const dynamicSuffix = cfg.conversationSuffixMode === "counter" ? getCounterSuffix(ctx?.sessionKey) : "";
58
+ const prefix = cfg.conversationIdPrefix || "";
59
+ const suffix = cfg.conversationIdSuffix || "";
60
+ if (base) return `${prefix}${base}${dynamicSuffix}${suffix}`;
61
+ return `${prefix}openclaw-${Date.now()}${dynamicSuffix}${suffix}`;
62
+ }
63
+
64
+ function buildSearchPayload(cfg, prompt, ctx) {
65
+ const queryRaw = `${cfg.queryPrefix || ""}${prompt}`;
66
+ const query =
67
+ Number.isFinite(cfg.maxQueryChars) && cfg.maxQueryChars > 0
68
+ ? queryRaw.slice(0, cfg.maxQueryChars)
69
+ : queryRaw;
70
+
71
+ const payload = {
72
+ user_id: cfg.userId,
73
+ query,
74
+ source: MEMOS_SOURCE,
75
+ };
76
+
77
+ if (!cfg.recallGlobal) {
78
+ const conversationId = resolveConversationId(cfg, ctx);
79
+ if (conversationId) payload.conversation_id = conversationId;
80
+ }
81
+
82
+ if (cfg.filter) payload.filter = cfg.filter;
83
+ if (cfg.knowledgebaseIds?.length) payload.knowledgebase_ids = cfg.knowledgebaseIds;
84
+
85
+ payload.memory_limit_number = cfg.memoryLimitNumber;
86
+ payload.include_preference = cfg.includePreference;
87
+ payload.preference_limit_number = cfg.preferenceLimitNumber;
88
+ payload.include_tool_memory = cfg.includeToolMemory;
89
+ payload.tool_memory_limit_number = cfg.toolMemoryLimitNumber;
90
+ payload.relativity = cfg.relativity;
91
+
92
+ return payload;
93
+ }
94
+
95
+ function buildAddMessagePayload(cfg, messages, ctx) {
96
+ const payload = {
97
+ user_id: cfg.userId,
98
+ conversation_id: resolveConversationId(cfg, ctx),
99
+ messages,
100
+ source: MEMOS_SOURCE,
101
+ };
102
+
103
+ if (cfg.agentId) payload.agent_id = cfg.agentId;
104
+ if (cfg.appId) payload.app_id = cfg.appId;
105
+ if (cfg.tags?.length) payload.tags = cfg.tags;
106
+
107
+ const info = {
108
+ source: "openclaw",
109
+ sessionKey: ctx?.sessionKey,
110
+ agentId: ctx?.agentId,
111
+ ...(cfg.info || {}),
112
+ };
113
+ if (Object.keys(info).length > 0) payload.info = info;
114
+
115
+ payload.allow_public = cfg.allowPublic;
116
+ if (cfg.allowKnowledgebaseIds?.length) payload.allow_knowledgebase_ids = cfg.allowKnowledgebaseIds;
117
+ payload.async_mode = cfg.asyncMode;
118
+
119
+ return payload;
120
+ }
121
+
122
+ function pickLastTurnMessages(messages, cfg) {
123
+ const lastUserIndex = messages
124
+ .map((m, idx) => ({ m, idx }))
125
+ .filter(({ m }) => m?.role === "user")
126
+ .map(({ idx }) => idx)
127
+ .pop();
128
+
129
+ if (lastUserIndex === undefined) return [];
130
+
131
+ const slice = messages.slice(lastUserIndex);
132
+ const results = [];
133
+
134
+ for (const msg of slice) {
135
+ if (!msg || !msg.role) continue;
136
+ if (msg.role === "user") {
137
+ const content = stripPrependedPrompt(extractText(msg.content));
138
+ if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
139
+ continue;
140
+ }
141
+ if (msg.role === "assistant" && cfg.includeAssistant) {
142
+ const content = extractText(msg.content);
143
+ if (content) results.push({ role: "assistant", content: truncate(content, cfg.maxMessageChars) });
144
+ }
145
+ }
146
+
147
+ return results;
148
+ }
149
+
150
+ function pickFullSessionMessages(messages, cfg) {
151
+ const results = [];
152
+ for (const msg of messages) {
153
+ if (!msg || !msg.role) continue;
154
+ if (msg.role === "user") {
155
+ const content = stripPrependedPrompt(extractText(msg.content));
156
+ if (content) results.push({ role: "user", content: truncate(content, cfg.maxMessageChars) });
157
+ }
158
+ if (msg.role === "assistant" && cfg.includeAssistant) {
159
+ const content = extractText(msg.content);
160
+ if (content) results.push({ role: "assistant", content: truncate(content, cfg.maxMessageChars) });
161
+ }
162
+ }
163
+ return results;
164
+ }
165
+
166
+ function truncate(text, maxLen) {
167
+ if (!text) return "";
168
+ if (!maxLen) return text;
169
+ return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
170
+ }
171
+
172
+ export default {
173
+ id: "memos-cloud-openclaw-plugin",
174
+ name: "MemOS Cloud OpenClaw Plugin",
175
+ description: "MemOS Cloud recall + add memory via lifecycle hooks",
176
+ kind: "lifecycle",
177
+
178
+ register(api) {
179
+ const cfg = buildConfig(api.pluginConfig);
180
+ const log = api.logger ?? console;
181
+
182
+ if (!cfg.envFileStatus?.found) {
183
+ const searchPaths = cfg.envFileStatus?.searchPaths?.join(", ") ?? ENV_FILE_SEARCH_HINTS.join(", ");
184
+ log.warn?.(`[memos-cloud] No .env found in ${searchPaths}; falling back to process env or plugin config.`);
185
+ }
186
+
187
+ if (cfg.conversationSuffixMode === "counter" && cfg.resetOnNew) {
188
+ if (api.config?.hooks?.internal?.enabled !== true) {
189
+ log.warn?.("[memos-cloud] command:new hook requires hooks.internal.enabled = true");
190
+ }
191
+ api.registerHook(
192
+ ["command:new"],
193
+ (event) => {
194
+ if (event?.type === "command" && event?.action === "new") {
195
+ bumpConversationCounter(event.sessionKey);
196
+ }
197
+ },
198
+ {
199
+ name: "memos-cloud-conversation-new",
200
+ description: "Increment MemOS conversation suffix on /new",
201
+ },
202
+ );
203
+ }
204
+
205
+ api.on("before_agent_start", async (event, ctx) => {
206
+ if (!cfg.recallEnabled) return;
207
+ if (!event?.prompt || event.prompt.length < 3) return;
208
+ if (!cfg.apiKey) {
209
+ warnMissingApiKey(log, "recall");
210
+ return;
211
+ }
212
+
213
+ try {
214
+ const payload = buildSearchPayload(cfg, event.prompt, ctx);
215
+ const result = await searchMemory(cfg, payload);
216
+ const promptBlock = formatPromptBlock(result, {
217
+ wrapTagBlocks: true,
218
+ relativity: payload.relativity
219
+ });
220
+ if (!promptBlock) return;
221
+
222
+ return {
223
+ prependContext: promptBlock,
224
+ };
225
+ } catch (err) {
226
+ log.warn?.(`[memos-cloud] recall failed: ${String(err)}`);
227
+ }
228
+ });
229
+
230
+ api.on("agent_end", async (event, ctx) => {
231
+ if (!cfg.addEnabled) return;
232
+ if (!event?.success || !event?.messages?.length) return;
233
+ if (!cfg.apiKey) {
234
+ warnMissingApiKey(log, "add");
235
+ return;
236
+ }
237
+
238
+ const now = Date.now();
239
+ if (cfg.throttleMs && now - lastCaptureTime < cfg.throttleMs) {
240
+ return;
241
+ }
242
+ lastCaptureTime = now;
243
+
244
+ try {
245
+ const messages =
246
+ cfg.captureStrategy === "full_session"
247
+ ? pickFullSessionMessages(event.messages, cfg)
248
+ : pickLastTurnMessages(event.messages, cfg);
249
+
250
+ if (!messages.length) return;
251
+
252
+ const payload = buildAddMessagePayload(cfg, messages, ctx);
253
+ await addMessage(cfg, payload);
254
+ } catch (err) {
255
+ log.warn?.(`[memos-cloud] add failed: ${String(err)}`);
256
+ }
257
+ });
258
+ },
259
+ };