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

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