@t0ken.ai/memoryx-openclaw-plugin 1.0.0 → 1.0.2

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # MemoryX OpenClaw Plugin
2
+
3
+ Real-time memory capture and recall plugin for OpenClaw.
4
+
5
+ ## Features
6
+
7
+ - **Conversation Buffer**: Automatically buffers conversations with token counting
8
+ - **Auto Registration**: Agents auto-register with machine fingerprint
9
+ - **Memory Recall**: Semantic search for relevant memories
10
+ - **Configurable API**: Custom API base URL support
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @t0ken.ai/memoryx-openclaw-plugin
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ ```json
21
+ {
22
+ "apiBaseUrl": "https://t0ken.ai/api"
23
+ }
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ The plugin automatically:
29
+ 1. Captures user and assistant messages
30
+ 2. Buffers conversations until threshold (2 rounds)
31
+ 3. Flushes to MemoryX API for memory extraction
32
+ 4. Recalls relevant memories before agent starts
33
+
34
+ ## License
35
+
36
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,14 +1,102 @@
1
1
  /**
2
- * MemoryX Real-time Plugin for OpenClaw
2
+ * MemoryX Realtime Plugin for OpenClaw - Phase 1
3
3
  *
4
4
  * Features:
5
- * 1. Real-time message capture to MemoryX
6
- * 2. Auto-recall memories before agent starts
7
- * 3. Compatible with memoryx-realtime-plugin (avoids duplication)
5
+ * - ConversationBuffer with token counting
6
+ * - Batch upload to /conversations/flush
7
+ * - Auto-register and quota handling
8
+ * - Sensitive data filtered on server
9
+ * - Configurable API base URL
10
+ * - Precise token counting with tiktoken
11
+ *
12
+ * Model Downloads (CDN):
13
+ * - INT8 Model (122MB, recommended): https://static.t0ken.ai/models/model_int8.onnx
14
+ * - FP32 Model (489MB): https://static.t0ken.ai/models/model.onnx
8
15
  */
16
+ interface PluginConfig {
17
+ apiBaseUrl?: string;
18
+ }
19
+ interface Message {
20
+ role: string;
21
+ content: string;
22
+ tokens: number;
23
+ timestamp: number;
24
+ }
25
+ interface RecallResult {
26
+ memories: Array<{
27
+ id: string;
28
+ content: string;
29
+ category: string;
30
+ score: number;
31
+ }>;
32
+ isLimited: boolean;
33
+ remainingQuota: number;
34
+ upgradeHint?: string;
35
+ }
36
+ declare class ConversationBuffer {
37
+ private messages;
38
+ private tokenCount;
39
+ private roundCount;
40
+ private lastRole;
41
+ private conversationId;
42
+ private startedAt;
43
+ private lastActivityAt;
44
+ private encoder;
45
+ private readonly ROUND_THRESHOLD;
46
+ private readonly TIMEOUT_MS;
47
+ private readonly MAX_TOKENS_PER_MESSAGE;
48
+ constructor();
49
+ private generateId;
50
+ private countTokens;
51
+ addMessage(role: string, content: string): boolean;
52
+ shouldFlush(): boolean;
53
+ flush(): {
54
+ conversation_id: string;
55
+ messages: Message[];
56
+ total_tokens: number;
57
+ };
58
+ forceFlush(): {
59
+ conversation_id: string;
60
+ messages: Message[];
61
+ total_tokens: number;
62
+ } | null;
63
+ getStatus(): {
64
+ messageCount: number;
65
+ tokenCount: number;
66
+ conversationId: string;
67
+ };
68
+ }
69
+ declare class MemoryXPlugin {
70
+ private config;
71
+ private buffer;
72
+ private flushTimer;
73
+ private readonly FLUSH_CHECK_INTERVAL;
74
+ private pluginConfig;
75
+ constructor(pluginConfig?: PluginConfig);
76
+ private get apiBase();
77
+ private init;
78
+ private loadConfig;
79
+ private saveConfig;
80
+ private autoRegister;
81
+ private getMachineFingerprint;
82
+ private startFlushTimer;
83
+ private flushConversation;
84
+ onMessage(role: string, content: string): Promise<boolean>;
85
+ recall(query: string, limit?: number): Promise<RecallResult>;
86
+ endConversation(): Promise<void>;
87
+ getStatus(): {
88
+ initialized: boolean;
89
+ hasApiKey: boolean;
90
+ bufferStatus: {
91
+ messageCount: number;
92
+ tokenCount: number;
93
+ };
94
+ };
95
+ }
9
96
  export declare function onMessage(message: string, context: Record<string, any>): Promise<{
10
97
  context: Record<string, any>;
11
98
  }>;
12
99
  export declare function onResponse(response: string, context: Record<string, any>): string;
13
- export declare function register(api: any): void;
100
+ export declare function register(api: any, pluginConfig?: PluginConfig): void;
101
+ export { MemoryXPlugin, ConversationBuffer };
14
102
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA6GH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC3B,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAmB3C;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC3B,MAAM,CAER;AAGD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,GAAG,QA0ChC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,164 +1,383 @@
1
1
  /**
2
- * MemoryX Real-time Plugin for OpenClaw
2
+ * MemoryX Realtime Plugin for OpenClaw - Phase 1
3
3
  *
4
4
  * Features:
5
- * 1. Real-time message capture to MemoryX
6
- * 2. Auto-recall memories before agent starts
7
- * 3. Compatible with memoryx-realtime-plugin (avoids duplication)
5
+ * - ConversationBuffer with token counting
6
+ * - Batch upload to /conversations/flush
7
+ * - Auto-register and quota handling
8
+ * - Sensitive data filtered on server
9
+ * - Configurable API base URL
10
+ * - Precise token counting with tiktoken
11
+ *
12
+ * Model Downloads (CDN):
13
+ * - INT8 Model (122MB, recommended): https://static.t0ken.ai/models/model_int8.onnx
14
+ * - FP32 Model (489MB): https://static.t0ken.ai/models/model.onnx
8
15
  */
9
- // MemoryX API configuration
10
- const MEMORYX_API_BASE = "http://t0ken.ai/api";
11
- // Check if memoryx-realtime-plugin is installed
12
- function isPluginInstalled() {
13
- try {
14
- const { execSync } = require("child_process");
15
- const result = execSync("openclaw plugins list", {
16
- encoding: "utf8",
17
- timeout: 5000,
18
- });
19
- return (result.includes("memoryx-realtime") && result.includes("loaded"));
20
- }
21
- catch (e) {
22
- return false;
16
+ import { getEncoding } from "js-tiktoken";
17
+ const DEFAULT_API_BASE = "https://t0ken.ai/api";
18
+ class ConversationBuffer {
19
+ messages = [];
20
+ tokenCount = 0;
21
+ roundCount = 0;
22
+ lastRole = "";
23
+ conversationId = "";
24
+ startedAt = Date.now();
25
+ lastActivityAt = Date.now();
26
+ encoder;
27
+ ROUND_THRESHOLD = 2;
28
+ TIMEOUT_MS = 30 * 60 * 1000;
29
+ MAX_TOKENS_PER_MESSAGE = 8000;
30
+ constructor() {
31
+ this.conversationId = this.generateId();
32
+ this.encoder = getEncoding("cl100k_base");
23
33
  }
24
- }
25
- // Store message to MemoryX
26
- async function storeToMemoryX(content, category = "semantic", metadata = {}) {
27
- try {
28
- const response = await fetch(`${MEMORYX_API_BASE}/memories`, {
29
- method: "POST",
30
- headers: {
31
- "Content-Type": "application/json",
32
- },
33
- body: JSON.stringify({
34
- content,
35
- category,
36
- project_id: "default",
37
- metadata: {
38
- ...metadata,
39
- source: "openclaw-realtime-plugin",
40
- timestamp: new Date().toISOString(),
41
- },
42
- }),
43
- });
44
- return response.ok;
34
+ generateId() {
35
+ return `conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
36
+ }
37
+ countTokens(text) {
38
+ return this.encoder.encode(text).length;
39
+ }
40
+ addMessage(role, content) {
41
+ if (!content || content.length < 2) {
42
+ return false;
43
+ }
44
+ const tokens = this.countTokens(content);
45
+ if (tokens > this.MAX_TOKENS_PER_MESSAGE) {
46
+ return false;
47
+ }
48
+ const message = {
49
+ role,
50
+ content,
51
+ tokens,
52
+ timestamp: Date.now()
53
+ };
54
+ this.messages.push(message);
55
+ this.tokenCount += tokens;
56
+ this.lastActivityAt = Date.now();
57
+ if (role === "assistant" && this.lastRole === "user") {
58
+ this.roundCount++;
59
+ }
60
+ this.lastRole = role;
61
+ return this.roundCount >= this.ROUND_THRESHOLD;
45
62
  }
46
- catch (error) {
63
+ shouldFlush() {
64
+ if (this.messages.length === 0) {
65
+ return false;
66
+ }
67
+ if (this.roundCount >= this.ROUND_THRESHOLD) {
68
+ return true;
69
+ }
70
+ const elapsed = Date.now() - this.lastActivityAt;
71
+ if (elapsed > this.TIMEOUT_MS) {
72
+ return true;
73
+ }
47
74
  return false;
48
75
  }
49
- }
50
- // Search MemoryX
51
- async function searchMemoryX(query, limit = 3) {
52
- try {
53
- const response = await fetch(`${MEMORYX_API_BASE}/memories/search`, {
54
- method: "POST",
55
- headers: {
56
- "Content-Type": "application/json",
57
- },
58
- body: JSON.stringify({
59
- query,
60
- limit,
61
- project_id: "default",
62
- }),
63
- });
64
- if (!response.ok)
65
- return [];
66
- const data = (await response.json());
67
- return data.data || [];
68
- }
69
- catch (error) {
70
- return [];
76
+ flush() {
77
+ const data = {
78
+ conversation_id: this.conversationId,
79
+ messages: [...this.messages],
80
+ total_tokens: this.tokenCount
81
+ };
82
+ this.messages = [];
83
+ this.tokenCount = 0;
84
+ this.roundCount = 0;
85
+ this.lastRole = "";
86
+ this.conversationId = this.generateId();
87
+ this.startedAt = Date.now();
88
+ this.lastActivityAt = Date.now();
89
+ return data;
90
+ }
91
+ forceFlush() {
92
+ if (this.messages.length === 0) {
93
+ return null;
94
+ }
95
+ return this.flush();
96
+ }
97
+ getStatus() {
98
+ return {
99
+ messageCount: this.messages.length,
100
+ tokenCount: this.tokenCount,
101
+ conversationId: this.conversationId
102
+ };
71
103
  }
72
104
  }
73
- // Check if content should be captured
74
- function shouldCapture(text) {
75
- if (!text || text.length < 5 || text.length > 500)
76
- return false;
77
- const skipPatterns = [
78
- /^[好的ok谢谢嗯啊哈哈你好hihello拜拜再见]{1,3}$/i,
79
- /^[??!!。,,\s]{1,5}$/,
80
- ];
81
- for (const pattern of skipPatterns) {
82
- if (pattern.test(text.trim()))
105
+ class MemoryXPlugin {
106
+ config = {
107
+ apiKey: null,
108
+ projectId: "default",
109
+ userId: null,
110
+ initialized: false,
111
+ apiBaseUrl: DEFAULT_API_BASE
112
+ };
113
+ buffer = new ConversationBuffer();
114
+ flushTimer = null;
115
+ FLUSH_CHECK_INTERVAL = 30000;
116
+ pluginConfig = null;
117
+ constructor(pluginConfig) {
118
+ this.pluginConfig = pluginConfig || null;
119
+ if (pluginConfig?.apiBaseUrl) {
120
+ this.config.apiBaseUrl = pluginConfig.apiBaseUrl;
121
+ }
122
+ this.init();
123
+ }
124
+ get apiBase() {
125
+ return this.config.apiBaseUrl || DEFAULT_API_BASE;
126
+ }
127
+ async init() {
128
+ await this.loadConfig();
129
+ if (!this.config.apiKey) {
130
+ await this.autoRegister();
131
+ }
132
+ this.startFlushTimer();
133
+ this.config.initialized = true;
134
+ }
135
+ async loadConfig() {
136
+ try {
137
+ const stored = localStorage.getItem("memoryx_config");
138
+ if (stored) {
139
+ const storedConfig = JSON.parse(stored);
140
+ this.config = {
141
+ ...this.config,
142
+ ...storedConfig,
143
+ apiBaseUrl: storedConfig.apiBaseUrl || this.config.apiBaseUrl
144
+ };
145
+ }
146
+ }
147
+ catch (e) {
148
+ console.warn("[MemoryX] Failed to load config:", e);
149
+ }
150
+ }
151
+ saveConfig() {
152
+ try {
153
+ localStorage.setItem("memoryx_config", JSON.stringify(this.config));
154
+ }
155
+ catch (e) {
156
+ console.warn("[MemoryX] Failed to save config:", e);
157
+ }
158
+ }
159
+ async autoRegister() {
160
+ try {
161
+ const fingerprint = await this.getMachineFingerprint();
162
+ const response = await fetch(`${this.apiBase}/agents/auto-register`, {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/json" },
165
+ body: JSON.stringify({
166
+ machine_fingerprint: fingerprint,
167
+ agent_type: "openclaw",
168
+ agent_name: "openclaw-agent",
169
+ platform: navigator.platform,
170
+ platform_version: navigator.userAgent
171
+ })
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error(`Auto-register failed: ${response.status}`);
175
+ }
176
+ const data = await response.json();
177
+ this.config.apiKey = data.api_key;
178
+ this.config.projectId = String(data.project_id);
179
+ this.config.userId = data.agent_id;
180
+ this.saveConfig();
181
+ console.log("[MemoryX] Auto-registered successfully");
182
+ }
183
+ catch (e) {
184
+ console.error("[MemoryX] Auto-register failed:", e);
185
+ }
186
+ }
187
+ async getMachineFingerprint() {
188
+ const components = [
189
+ navigator.platform,
190
+ navigator.language,
191
+ navigator.hardwareConcurrency || 0,
192
+ screen.width,
193
+ screen.height,
194
+ new Date().getTimezoneOffset()
195
+ ];
196
+ const raw = components.join("|");
197
+ const encoder = new TextEncoder();
198
+ const data = encoder.encode(raw);
199
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
200
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
201
+ return hashArray.slice(0, 32).map(b => b.toString(16).padStart(2, "0")).join("");
202
+ }
203
+ startFlushTimer() {
204
+ this.flushTimer = setInterval(() => {
205
+ if (this.buffer.shouldFlush()) {
206
+ this.flushConversation();
207
+ }
208
+ }, this.FLUSH_CHECK_INTERVAL);
209
+ }
210
+ async flushConversation() {
211
+ if (!this.config.apiKey) {
212
+ return;
213
+ }
214
+ const data = this.buffer.forceFlush();
215
+ if (!data || data.messages.length === 0) {
216
+ return;
217
+ }
218
+ try {
219
+ const response = await fetch(`${this.apiBase}/v1/conversations/flush`, {
220
+ method: "POST",
221
+ headers: {
222
+ "Content-Type": "application/json",
223
+ "X-API-Key": this.config.apiKey
224
+ },
225
+ body: JSON.stringify(data)
226
+ });
227
+ if (!response.ok) {
228
+ const errorData = await response.json().catch(() => ({}));
229
+ if (response.status === 402) {
230
+ console.warn("[MemoryX] Quota exceeded:", errorData.detail);
231
+ }
232
+ else {
233
+ console.error("[MemoryX] Flush failed:", errorData);
234
+ }
235
+ }
236
+ else {
237
+ const result = await response.json();
238
+ console.log(`[MemoryX] Flushed ${data.messages.length} messages, extracted ${result.extracted_count} memories`);
239
+ }
240
+ }
241
+ catch (e) {
242
+ console.error("[MemoryX] Flush error:", e);
243
+ }
244
+ }
245
+ async onMessage(role, content) {
246
+ if (!content || content.length < 2) {
83
247
  return false;
248
+ }
249
+ const skipPatterns = [
250
+ /^[好的ok谢谢嗯啊哈哈你好hihello拜拜再见]{1,5}$/i,
251
+ /^[??!!。,,\s]{1,10}$/
252
+ ];
253
+ for (const pattern of skipPatterns) {
254
+ if (pattern.test(content.trim())) {
255
+ return false;
256
+ }
257
+ }
258
+ const shouldFlush = this.buffer.addMessage(role, content);
259
+ if (shouldFlush) {
260
+ await this.flushConversation();
261
+ }
262
+ return true;
263
+ }
264
+ async recall(query, limit = 5) {
265
+ if (!this.config.apiKey || !query || query.length < 2) {
266
+ return { memories: [], isLimited: false, remainingQuota: 0 };
267
+ }
268
+ try {
269
+ const response = await fetch(`${this.apiBase}/v1/memories/search`, {
270
+ method: "POST",
271
+ headers: {
272
+ "Content-Type": "application/json",
273
+ "X-API-Key": this.config.apiKey
274
+ },
275
+ body: JSON.stringify({
276
+ query,
277
+ project_id: this.config.projectId,
278
+ limit
279
+ })
280
+ });
281
+ if (!response.ok) {
282
+ const errorData = await response.json().catch(() => ({}));
283
+ if (response.status === 402 || response.status === 429) {
284
+ return {
285
+ memories: [],
286
+ isLimited: true,
287
+ remainingQuota: 0,
288
+ upgradeHint: errorData.detail || "云端查询配额已用尽,请升级到付费版"
289
+ };
290
+ }
291
+ throw new Error(`Search failed: ${response.status}`);
292
+ }
293
+ const data = await response.json();
294
+ return {
295
+ memories: (data.data || []).map((m) => ({
296
+ id: m.id,
297
+ content: m.content,
298
+ category: m.category || "other",
299
+ score: m.score || 0.5
300
+ })),
301
+ isLimited: false,
302
+ remainingQuota: data.remaining_quota ?? -1
303
+ };
304
+ }
305
+ catch (e) {
306
+ console.error("[MemoryX] Recall failed:", e);
307
+ return { memories: [], isLimited: false, remainingQuota: 0 };
308
+ }
309
+ }
310
+ async endConversation() {
311
+ await this.flushConversation();
312
+ console.log("[MemoryX] Conversation ended, buffer flushed");
313
+ }
314
+ getStatus() {
315
+ return {
316
+ initialized: this.config.initialized,
317
+ hasApiKey: !!this.config.apiKey,
318
+ bufferStatus: this.buffer.getStatus()
319
+ };
84
320
  }
85
- const triggers = [
86
- /记住|记一下|别忘了|save|remember/i,
87
- /我喜欢|我讨厌|我习惯|我偏好|prefer|like|hate/i,
88
- /我是|我在|我来自|i am|i work/i,
89
- /纠正|更正|应该是|correct|actually/i,
90
- /计划|打算|目标|plan|goal|will/i,
91
- ];
92
- return triggers.some((pattern) => pattern.test(text));
93
- }
94
- // Detect category
95
- function detectCategory(text) {
96
- const lower = text.toLowerCase();
97
- if (/prefer|like|hate|习惯|偏好|喜欢|讨厌/.test(lower))
98
- return "preference";
99
- if (/correct|纠正|更正/.test(lower))
100
- return "correction";
101
- if (/plan|goal|计划|打算/.test(lower))
102
- return "plan";
103
- return "semantic";
104
321
  }
105
- // OpenClaw Hook handlers
322
+ let plugin;
106
323
  export async function onMessage(message, context) {
107
- // Skip if memoryx-realtime-plugin is installed (avoid duplication)
108
- if (isPluginInstalled()) {
109
- return { context };
110
- }
111
- if (!shouldCapture(message)) {
112
- return { context };
113
- }
114
- const category = detectCategory(message);
115
- // Async store (non-blocking)
116
- storeToMemoryX(message, category, {
117
- from: context?.from,
118
- channel: context?.channelId,
119
- }).catch(() => { });
324
+ if (message && plugin) {
325
+ await plugin.onMessage("user", message);
326
+ }
120
327
  return { context };
121
328
  }
122
329
  export function onResponse(response, context) {
123
330
  return response;
124
331
  }
125
- // Lifecycle hooks for OpenClaw Extension
126
- export function register(api) {
127
- api.logger.info("[MemoryX Realtime] Plugin registering...");
128
- // 1. Message capture
332
+ export function register(api, pluginConfig) {
333
+ api.logger.info("[MemoryX] Plugin registering (Phase 1 - Cloud with Buffer)...");
334
+ if (pluginConfig?.apiBaseUrl) {
335
+ api.logger.info(`[MemoryX] Using custom API base URL: ${pluginConfig.apiBaseUrl}`);
336
+ }
337
+ plugin = new MemoryXPlugin(pluginConfig);
129
338
  api.on("message_received", async (event, ctx) => {
130
- if (isPluginInstalled())
131
- return;
132
339
  const { content, from, timestamp } = event;
133
- if (!shouldCapture(content))
134
- return;
135
- const category = detectCategory(content);
136
- await storeToMemoryX(content, category, {
137
- from,
138
- channel: ctx.channelId,
139
- timestamp,
140
- });
340
+ if (content && plugin) {
341
+ await plugin.onMessage("user", content);
342
+ }
343
+ });
344
+ api.on("assistant_response", async (event, ctx) => {
345
+ const { content } = event;
346
+ if (content && plugin) {
347
+ await plugin.onMessage("assistant", content);
348
+ }
141
349
  });
142
- // 2. Auto-recall before agent starts
143
350
  api.on("before_agent_start", async (event, ctx) => {
144
351
  const { prompt } = event;
145
- if (!prompt || prompt.length < 5)
352
+ if (!prompt || prompt.length < 2 || !plugin)
146
353
  return;
147
354
  try {
148
- const results = await searchMemoryX(prompt, 3);
149
- if (results.length === 0)
355
+ const result = await plugin.recall(prompt, 5);
356
+ if (result.isLimited) {
357
+ api.logger.warn(`[MemoryX] ${result.upgradeHint}`);
358
+ return {
359
+ prependContext: `[系统提示] ${result.upgradeHint}\n`
360
+ };
361
+ }
362
+ if (result.memories.length === 0)
150
363
  return;
151
- const memories = results
152
- .map((r) => `- [${r.category}] ${r.content}`)
364
+ const memories = result.memories
365
+ .map(m => `- [${m.category}] ${m.content}`)
153
366
  .join("\n");
154
- api.logger.info(`[MemoryX] Recalled ${results.length} memories`);
367
+ api.logger.info(`[MemoryX] Recalled ${result.memories.length} memories from cloud`);
155
368
  return {
156
- prependContext: `[相关记忆]\n${memories}\n[End of memories]`,
369
+ prependContext: `[相关记忆]\n${memories}\n[End of memories]\n`
157
370
  };
158
371
  }
159
372
  catch (error) {
160
373
  api.logger.warn(`[MemoryX] Recall failed: ${error}`);
161
374
  }
162
375
  });
163
- api.logger.info("[MemoryX Realtime] Plugin registered successfully");
376
+ api.on("conversation_end", async (event, ctx) => {
377
+ if (plugin) {
378
+ await plugin.endConversation();
379
+ }
380
+ });
381
+ api.logger.info("[MemoryX] Plugin registered successfully");
164
382
  }
383
+ export { MemoryXPlugin, ConversationBuffer };
@@ -1,7 +1,7 @@
1
1
  {
2
- "id": "memoryx-realtime-plugin",
2
+ "id": "@t0ken.ai/memoryx-openclaw-plugin",
3
3
  "name": "MemoryX Real-time Plugin",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "description": "Real-time memory capture and recall for OpenClaw",
6
6
  "entry": "./dist/index.js",
7
7
  "kind": "memory",
package/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "@t0ken.ai/memoryx-openclaw-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MemoryX real-time memory capture and recall plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "openclaw.plugin.json",
11
+ "README.md"
12
+ ],
8
13
  "scripts": {
9
14
  "build": "tsc",
10
15
  "prepare": "npm run build"
11
16
  },
12
- "keywords": ["openclaw", "memoryx", "memory", "plugin"],
17
+ "keywords": [
18
+ "openclaw",
19
+ "memoryx",
20
+ "memory",
21
+ "plugin"
22
+ ],
13
23
  "author": "MemoryX Team",
14
24
  "license": "MIT",
15
25
  "repository": {
@@ -18,10 +28,18 @@
18
28
  "directory": "plugins/memoryx-realtime-plugin"
19
29
  },
20
30
  "openclaw": {
21
- "extensions": ["./dist/index.js"]
31
+ "extensions": [
32
+ "./dist/index.js"
33
+ ]
22
34
  },
23
35
  "devDependencies": {
24
- "typescript": "^5.0.0",
25
- "@types/node": "^20.0.0"
36
+ "@types/node": "^20.0.0",
37
+ "typescript": "^5.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@huggingface/transformers": "^3.8.1",
41
+ "js-tiktoken": "^1.0.21",
42
+ "onnxruntime-node": "^1.24.1",
43
+ "onnxruntime-web": "^1.24.1"
26
44
  }
27
- }
45
+ }
package/src/index.ts DELETED
@@ -1,191 +0,0 @@
1
- /**
2
- * MemoryX Real-time Plugin for OpenClaw
3
- *
4
- * Features:
5
- * 1. Real-time message capture to MemoryX
6
- * 2. Auto-recall memories before agent starts
7
- * 3. Compatible with memoryx-realtime-plugin (avoids duplication)
8
- */
9
-
10
- // MemoryX API configuration
11
- const MEMORYX_API_BASE = "http://t0ken.ai/api";
12
-
13
- // Check if memoryx-realtime-plugin is installed
14
- function isPluginInstalled(): boolean {
15
- try {
16
- const { execSync } = require("child_process");
17
- const result = execSync("openclaw plugins list", {
18
- encoding: "utf8",
19
- timeout: 5000,
20
- });
21
- return (
22
- result.includes("memoryx-realtime") && result.includes("loaded")
23
- );
24
- } catch (e) {
25
- return false;
26
- }
27
- }
28
-
29
- // Store message to MemoryX
30
- async function storeToMemoryX(
31
- content: string,
32
- category: string = "semantic",
33
- metadata: Record<string, any> = {}
34
- ): Promise<boolean> {
35
- try {
36
- const response = await fetch(`${MEMORYX_API_BASE}/memories`, {
37
- method: "POST",
38
- headers: {
39
- "Content-Type": "application/json",
40
- },
41
- body: JSON.stringify({
42
- content,
43
- category,
44
- project_id: "default",
45
- metadata: {
46
- ...metadata,
47
- source: "openclaw-realtime-plugin",
48
- timestamp: new Date().toISOString(),
49
- },
50
- }),
51
- });
52
- return response.ok;
53
- } catch (error) {
54
- return false;
55
- }
56
- }
57
-
58
- // Search MemoryX
59
- async function searchMemoryX(
60
- query: string,
61
- limit: number = 3
62
- ): Promise<Array<{ id: string; content: string; category: string; similarity: number }>> {
63
- try {
64
- const response = await fetch(`${MEMORYX_API_BASE}/memories/search`, {
65
- method: "POST",
66
- headers: {
67
- "Content-Type": "application/json",
68
- },
69
- body: JSON.stringify({
70
- query,
71
- limit,
72
- project_id: "default",
73
- }),
74
- });
75
- if (!response.ok) return [];
76
- const data = (await response.json()) as { data?: any[] };
77
- return data.data || [];
78
- } catch (error) {
79
- return [];
80
- }
81
- }
82
-
83
- // Check if content should be captured
84
- function shouldCapture(text: string): boolean {
85
- if (!text || text.length < 5 || text.length > 500) return false;
86
-
87
- const skipPatterns = [
88
- /^[好的ok谢谢嗯啊哈哈你好hihello拜拜再见]{1,3}$/i,
89
- /^[??!!。,,\s]{1,5}$/,
90
- ];
91
-
92
- for (const pattern of skipPatterns) {
93
- if (pattern.test(text.trim())) return false;
94
- }
95
-
96
- const triggers = [
97
- /记住|记一下|别忘了|save|remember/i,
98
- /我喜欢|我讨厌|我习惯|我偏好|prefer|like|hate/i,
99
- /我是|我在|我来自|i am|i work/i,
100
- /纠正|更正|应该是|correct|actually/i,
101
- /计划|打算|目标|plan|goal|will/i,
102
- ];
103
-
104
- return triggers.some((pattern) => pattern.test(text));
105
- }
106
-
107
- // Detect category
108
- function detectCategory(text: string): string {
109
- const lower = text.toLowerCase();
110
- if (/prefer|like|hate|习惯|偏好|喜欢|讨厌/.test(lower)) return "preference";
111
- if (/correct|纠正|更正/.test(lower)) return "correction";
112
- if (/plan|goal|计划|打算/.test(lower)) return "plan";
113
- return "semantic";
114
- }
115
-
116
- // OpenClaw Hook handlers
117
- export async function onMessage(
118
- message: string,
119
- context: Record<string, any>
120
- ): Promise<{ context: Record<string, any> }> {
121
- // Skip if memoryx-realtime-plugin is installed (avoid duplication)
122
- if (isPluginInstalled()) {
123
- return { context };
124
- }
125
-
126
- if (!shouldCapture(message)) {
127
- return { context };
128
- }
129
-
130
- const category = detectCategory(message);
131
-
132
- // Async store (non-blocking)
133
- storeToMemoryX(message, category, {
134
- from: context?.from,
135
- channel: context?.channelId,
136
- }).catch(() => {});
137
-
138
- return { context };
139
- }
140
-
141
- export function onResponse(
142
- response: string,
143
- context: Record<string, any>
144
- ): string {
145
- return response;
146
- }
147
-
148
- // Lifecycle hooks for OpenClaw Extension
149
- export function register(api: any) {
150
- api.logger.info("[MemoryX Realtime] Plugin registering...");
151
-
152
- // 1. Message capture
153
- api.on("message_received", async (event: any, ctx: any) => {
154
- if (isPluginInstalled()) return;
155
-
156
- const { content, from, timestamp } = event;
157
- if (!shouldCapture(content)) return;
158
-
159
- const category = detectCategory(content);
160
- await storeToMemoryX(content, category, {
161
- from,
162
- channel: ctx.channelId,
163
- timestamp,
164
- });
165
- });
166
-
167
- // 2. Auto-recall before agent starts
168
- api.on("before_agent_start", async (event: any, ctx: any) => {
169
- const { prompt } = event;
170
- if (!prompt || prompt.length < 5) return;
171
-
172
- try {
173
- const results = await searchMemoryX(prompt, 3);
174
- if (results.length === 0) return;
175
-
176
- const memories = results
177
- .map((r) => `- [${r.category}] ${r.content}`)
178
- .join("\n");
179
-
180
- api.logger.info(`[MemoryX] Recalled ${results.length} memories`);
181
-
182
- return {
183
- prependContext: `[相关记忆]\n${memories}\n[End of memories]`,
184
- };
185
- } catch (error) {
186
- api.logger.warn(`[MemoryX] Recall failed: ${error}`);
187
- }
188
- });
189
-
190
- api.logger.info("[MemoryX Realtime] Plugin registered successfully");
191
- }
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "lib": ["ES2022"],
7
- "outDir": "./dist",
8
- "rootDir": "./src",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "declaration": true,
14
- "declarationMap": true
15
- },
16
- "include": ["src/**/*"]
17
- }