@memoryrelay/plugin-memoryrelay-ai 0.5.3 → 0.6.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/LICENSE +1 -1
- package/README.md +184 -513
- package/index.ts +246 -62
- package/openclaw.plugin.json +25 -8
- package/package.json +11 -4
package/index.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw Memory Plugin - MemoryRelay
|
|
3
|
+
* Version: 0.6.0 (Enhanced)
|
|
3
4
|
*
|
|
4
5
|
* Long-term memory with vector search using MemoryRelay API.
|
|
5
6
|
* Provides auto-recall and auto-capture via lifecycle hooks.
|
|
6
7
|
*
|
|
7
8
|
* API: https://api.memoryrelay.net
|
|
8
9
|
* Docs: https://memoryrelay.io
|
|
10
|
+
*
|
|
11
|
+
* ENHANCEMENTS (v0.6.0):
|
|
12
|
+
* - Retry logic with exponential backoff (3 attempts)
|
|
13
|
+
* - Request timeout (30 seconds)
|
|
14
|
+
* - Environment variable fallback support
|
|
15
|
+
* - Channel filtering (excludeChannels config)
|
|
16
|
+
* - Additional CLI commands (stats, delete, export)
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
19
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
@@ -16,19 +24,23 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
16
24
|
|
|
17
25
|
const DEFAULT_API_URL = "https://api.memoryrelay.net";
|
|
18
26
|
const VALID_HEALTH_STATUSES = ["ok", "healthy", "up"];
|
|
27
|
+
const REQUEST_TIMEOUT_MS = 30000; // 30 seconds
|
|
28
|
+
const MAX_RETRIES = 3;
|
|
29
|
+
const INITIAL_RETRY_DELAY_MS = 1000; // 1 second
|
|
19
30
|
|
|
20
31
|
// ============================================================================
|
|
21
32
|
// Types
|
|
22
33
|
// ============================================================================
|
|
23
34
|
|
|
24
35
|
interface MemoryRelayConfig {
|
|
25
|
-
apiKey
|
|
26
|
-
agentId
|
|
36
|
+
apiKey?: string; // Optional now (can use env var)
|
|
37
|
+
agentId?: string; // Optional now (can use env var)
|
|
27
38
|
apiUrl?: string;
|
|
28
39
|
autoCapture?: boolean;
|
|
29
40
|
autoRecall?: boolean;
|
|
30
41
|
recallLimit?: number;
|
|
31
42
|
recallThreshold?: number;
|
|
43
|
+
excludeChannels?: string[]; // NEW: Channels to skip auto-recall
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
interface Memory {
|
|
@@ -47,8 +59,68 @@ interface SearchResult {
|
|
|
47
59
|
score: number;
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
interface Stats {
|
|
63
|
+
total_memories: number;
|
|
64
|
+
last_updated?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
// ============================================================================
|
|
51
|
-
//
|
|
68
|
+
// Utility Functions
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sleep for specified milliseconds
|
|
73
|
+
*/
|
|
74
|
+
function sleep(ms: number): Promise<void> {
|
|
75
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if error is retryable (network/timeout errors)
|
|
80
|
+
*/
|
|
81
|
+
function isRetryableError(error: unknown): boolean {
|
|
82
|
+
const errStr = String(error).toLowerCase();
|
|
83
|
+
return (
|
|
84
|
+
errStr.includes("timeout") ||
|
|
85
|
+
errStr.includes("econnrefused") ||
|
|
86
|
+
errStr.includes("enotfound") ||
|
|
87
|
+
errStr.includes("network") ||
|
|
88
|
+
errStr.includes("fetch failed") ||
|
|
89
|
+
errStr.includes("502") ||
|
|
90
|
+
errStr.includes("503") ||
|
|
91
|
+
errStr.includes("504")
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Fetch with timeout
|
|
97
|
+
*/
|
|
98
|
+
async function fetchWithTimeout(
|
|
99
|
+
url: string,
|
|
100
|
+
options: RequestInit,
|
|
101
|
+
timeoutMs: number,
|
|
102
|
+
): Promise<Response> {
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
...options,
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
return response;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
if ((err as Error).name === "AbortError") {
|
|
116
|
+
throw new Error("Request timeout");
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// MemoryRelay API Client (Enhanced)
|
|
52
124
|
// ============================================================================
|
|
53
125
|
|
|
54
126
|
class MemoryRelayClient {
|
|
@@ -58,32 +130,60 @@ class MemoryRelayClient {
|
|
|
58
130
|
private readonly apiUrl: string = DEFAULT_API_URL,
|
|
59
131
|
) {}
|
|
60
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Make HTTP request with retry logic and timeout
|
|
135
|
+
*/
|
|
61
136
|
private async request<T>(
|
|
62
137
|
method: string,
|
|
63
138
|
path: string,
|
|
64
139
|
body?: unknown,
|
|
140
|
+
retryCount = 0,
|
|
65
141
|
): Promise<T> {
|
|
66
142
|
const url = `${this.apiUrl}${path}`;
|
|
67
143
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
`MemoryRelay API error: ${response.status} ${response.statusText}` +
|
|
82
|
-
(errorData.message ? ` - ${errorData.message}` : ""),
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetchWithTimeout(
|
|
146
|
+
url,
|
|
147
|
+
{
|
|
148
|
+
method,
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
152
|
+
"User-Agent": "openclaw-memory-memoryrelay/0.6.0",
|
|
153
|
+
},
|
|
154
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
155
|
+
},
|
|
156
|
+
REQUEST_TIMEOUT_MS,
|
|
83
157
|
);
|
|
84
|
-
}
|
|
85
158
|
|
|
86
|
-
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const errorData = await response.json().catch(() => ({}));
|
|
161
|
+
const error = new Error(
|
|
162
|
+
`MemoryRelay API error: ${response.status} ${response.statusText}` +
|
|
163
|
+
(errorData.message ? ` - ${errorData.message}` : ""),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Retry on 5xx errors
|
|
167
|
+
if (response.status >= 500 && retryCount < MAX_RETRIES) {
|
|
168
|
+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
169
|
+
await sleep(delay);
|
|
170
|
+
return this.request<T>(method, path, body, retryCount + 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return response.json();
|
|
177
|
+
} catch (err) {
|
|
178
|
+
// Retry on network errors
|
|
179
|
+
if (isRetryableError(err) && retryCount < MAX_RETRIES) {
|
|
180
|
+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retryCount);
|
|
181
|
+
await sleep(delay);
|
|
182
|
+
return this.request<T>(method, path, body, retryCount + 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
87
187
|
}
|
|
88
188
|
|
|
89
189
|
async store(content: string, metadata?: Record<string, string>): Promise<Memory> {
|
|
@@ -132,8 +232,8 @@ class MemoryRelayClient {
|
|
|
132
232
|
return this.request<{ status: string }>("GET", "/v1/health");
|
|
133
233
|
}
|
|
134
234
|
|
|
135
|
-
async stats(): Promise<
|
|
136
|
-
const response = await this.request<{ data:
|
|
235
|
+
async stats(): Promise<Stats> {
|
|
236
|
+
const response = await this.request<{ data: Stats }>(
|
|
137
237
|
"GET",
|
|
138
238
|
`/v1/stats?agent_id=${encodeURIComponent(this.agentId)}`,
|
|
139
239
|
);
|
|
@@ -142,6 +242,25 @@ class MemoryRelayClient {
|
|
|
142
242
|
last_updated: response.data?.last_updated,
|
|
143
243
|
};
|
|
144
244
|
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Export all memories as JSON
|
|
248
|
+
*/
|
|
249
|
+
async export(): Promise<Memory[]> {
|
|
250
|
+
const allMemories: Memory[] = [];
|
|
251
|
+
let offset = 0;
|
|
252
|
+
const limit = 100;
|
|
253
|
+
|
|
254
|
+
while (true) {
|
|
255
|
+
const batch = await this.list(limit, offset);
|
|
256
|
+
if (batch.length === 0) break;
|
|
257
|
+
allMemories.push(...batch);
|
|
258
|
+
offset += limit;
|
|
259
|
+
if (batch.length < limit) break; // Last page
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return allMemories;
|
|
263
|
+
}
|
|
145
264
|
}
|
|
146
265
|
|
|
147
266
|
// ============================================================================
|
|
@@ -171,59 +290,60 @@ function shouldCapture(text: string): boolean {
|
|
|
171
290
|
|
|
172
291
|
export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
173
292
|
const cfg = api.pluginConfig as MemoryRelayConfig | undefined;
|
|
174
|
-
|
|
175
|
-
|
|
293
|
+
|
|
294
|
+
// NEW: Fall back to environment variables
|
|
295
|
+
const apiKey = cfg?.apiKey || process.env.MEMORYRELAY_API_KEY;
|
|
296
|
+
const agentId = cfg?.agentId || process.env.MEMORYRELAY_AGENT_ID || api.agentName;
|
|
297
|
+
|
|
298
|
+
if (!apiKey) {
|
|
176
299
|
api.logger.error(
|
|
177
|
-
"memory-memoryrelay: Missing API key in config.\n\n" +
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
300
|
+
"memory-memoryrelay: Missing API key in config or MEMORYRELAY_API_KEY env var.\n\n" +
|
|
301
|
+
"REQUIRED: Add config after installation:\n\n" +
|
|
302
|
+
'cat ~/.openclaw/openclaw.json | jq \'.plugins.entries."plugin-memoryrelay-ai".config = {\n' +
|
|
303
|
+
' "apiKey": "YOUR_API_KEY",\n' +
|
|
304
|
+
' "agentId": "YOUR_AGENT_ID"\n' +
|
|
305
|
+
"}' > /tmp/config.json && mv /tmp/config.json ~/.openclaw/openclaw.json\n\n" +
|
|
306
|
+
"Or set environment variable:\n" +
|
|
307
|
+
'export MEMORYRELAY_API_KEY="mem_prod_..."\n\n' +
|
|
308
|
+
"Then restart: openclaw gateway restart\n\n" +
|
|
309
|
+
"Get your API key from: https://memoryrelay.ai",
|
|
185
310
|
);
|
|
186
311
|
return;
|
|
187
312
|
}
|
|
188
|
-
|
|
189
|
-
if (!
|
|
190
|
-
api.logger.error("memory-memoryrelay: Missing agentId in config");
|
|
313
|
+
|
|
314
|
+
if (!agentId) {
|
|
315
|
+
api.logger.error("memory-memoryrelay: Missing agentId in config or MEMORYRELAY_AGENT_ID env var");
|
|
191
316
|
return;
|
|
192
317
|
}
|
|
193
|
-
|
|
194
|
-
const apiUrl = cfg.apiUrl || DEFAULT_API_URL;
|
|
195
|
-
const client = new MemoryRelayClient(cfg.apiKey, cfg.agentId, apiUrl);
|
|
196
318
|
|
|
197
|
-
|
|
319
|
+
const apiUrl = cfg?.apiUrl || process.env.MEMORYRELAY_API_URL || DEFAULT_API_URL;
|
|
320
|
+
const client = new MemoryRelayClient(apiKey, agentId, apiUrl);
|
|
321
|
+
|
|
322
|
+
// Verify connection on startup (with timeout)
|
|
198
323
|
try {
|
|
199
324
|
await client.health();
|
|
200
325
|
api.logger.info(`memory-memoryrelay: connected to ${apiUrl}`);
|
|
201
326
|
} catch (err) {
|
|
202
327
|
api.logger.error(`memory-memoryrelay: health check failed: ${String(err)}`);
|
|
203
|
-
|
|
328
|
+
// Continue loading plugin even if health check fails (will retry on first use)
|
|
204
329
|
}
|
|
205
330
|
|
|
206
331
|
// ========================================================================
|
|
207
332
|
// Status Reporting (for openclaw status command)
|
|
208
333
|
// ========================================================================
|
|
209
334
|
|
|
210
|
-
// Register gateway RPC method for status probing
|
|
211
|
-
// This allows OpenClaw's status command to query plugin availability
|
|
212
335
|
api.registerGatewayMethod?.("memory.status", async ({ respond }) => {
|
|
213
336
|
try {
|
|
214
337
|
const health = await client.health();
|
|
215
338
|
let memoryCount = 0;
|
|
216
339
|
|
|
217
|
-
// Try to get stats if the endpoint exists
|
|
218
340
|
try {
|
|
219
341
|
const stats = await client.stats();
|
|
220
342
|
memoryCount = stats.total_memories;
|
|
221
343
|
} catch (statsErr) {
|
|
222
|
-
// Stats endpoint may not exist yet - that's okay, just report 0
|
|
223
344
|
api.logger.debug?.(`memory-memoryrelay: stats endpoint unavailable: ${String(statsErr)}`);
|
|
224
345
|
}
|
|
225
346
|
|
|
226
|
-
// Consider API connected if health check succeeds with any recognized status
|
|
227
347
|
const healthStatus = String(health.status).toLowerCase();
|
|
228
348
|
const isConnected = VALID_HEALTH_STATUSES.includes(healthStatus);
|
|
229
349
|
|
|
@@ -233,7 +353,6 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
233
353
|
endpoint: apiUrl,
|
|
234
354
|
memoryCount: memoryCount,
|
|
235
355
|
agentId: agentId,
|
|
236
|
-
// OpenClaw checks status.vector.available for memory plugins
|
|
237
356
|
vector: {
|
|
238
357
|
available: true,
|
|
239
358
|
enabled: true,
|
|
@@ -246,7 +365,6 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
246
365
|
error: String(err),
|
|
247
366
|
endpoint: apiUrl,
|
|
248
367
|
agentId: agentId,
|
|
249
|
-
// Report vector as unavailable when API fails
|
|
250
368
|
vector: {
|
|
251
369
|
available: false,
|
|
252
370
|
enabled: true,
|
|
@@ -328,7 +446,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
328
446
|
},
|
|
329
447
|
execute: async (_id, { query, limit = 5 }: { query: string; limit?: number }) => {
|
|
330
448
|
try {
|
|
331
|
-
const results = await client.search(query, limit, cfg
|
|
449
|
+
const results = await client.search(query, limit, cfg?.recallThreshold || 0.3);
|
|
332
450
|
|
|
333
451
|
if (results.length === 0) {
|
|
334
452
|
return {
|
|
@@ -453,7 +571,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
453
571
|
);
|
|
454
572
|
|
|
455
573
|
// ========================================================================
|
|
456
|
-
// CLI Commands
|
|
574
|
+
// CLI Commands (Enhanced)
|
|
457
575
|
// ========================================================================
|
|
458
576
|
|
|
459
577
|
api.registerCli(
|
|
@@ -466,24 +584,48 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
466
584
|
.action(async () => {
|
|
467
585
|
try {
|
|
468
586
|
const health = await client.health();
|
|
587
|
+
const stats = await client.stats();
|
|
469
588
|
console.log(`Status: ${health.status}`);
|
|
470
589
|
console.log(`Agent ID: ${agentId}`);
|
|
471
590
|
console.log(`API: ${apiUrl}`);
|
|
591
|
+
console.log(`Total Memories: ${stats.total_memories}`);
|
|
592
|
+
if (stats.last_updated) {
|
|
593
|
+
console.log(`Last Updated: ${new Date(stats.last_updated).toLocaleString()}`);
|
|
594
|
+
}
|
|
472
595
|
} catch (err) {
|
|
473
596
|
console.error(`Connection failed: ${String(err)}`);
|
|
474
597
|
}
|
|
475
598
|
});
|
|
476
599
|
|
|
600
|
+
mem
|
|
601
|
+
.command("stats")
|
|
602
|
+
.description("Show agent statistics")
|
|
603
|
+
.action(async () => {
|
|
604
|
+
try {
|
|
605
|
+
const stats = await client.stats();
|
|
606
|
+
console.log(`Total Memories: ${stats.total_memories}`);
|
|
607
|
+
if (stats.last_updated) {
|
|
608
|
+
console.log(`Last Updated: ${new Date(stats.last_updated).toLocaleString()}`);
|
|
609
|
+
}
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.error(`Failed to fetch stats: ${String(err)}`);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
477
615
|
mem
|
|
478
616
|
.command("list")
|
|
479
617
|
.description("List recent memories")
|
|
480
618
|
.option("--limit <n>", "Max results", "10")
|
|
481
619
|
.action(async (opts) => {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
620
|
+
try {
|
|
621
|
+
const memories = await client.list(parseInt(opts.limit));
|
|
622
|
+
for (const m of memories) {
|
|
623
|
+
console.log(`[${m.id.slice(0, 8)}] ${m.content.slice(0, 80)}...`);
|
|
624
|
+
}
|
|
625
|
+
console.log(`\nTotal: ${memories.length} memories`);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
console.error(`Failed to list memories: ${String(err)}`);
|
|
485
628
|
}
|
|
486
|
-
console.log(`\nTotal: ${memories.length} memories`);
|
|
487
629
|
});
|
|
488
630
|
|
|
489
631
|
mem
|
|
@@ -492,9 +634,42 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
492
634
|
.argument("<query>", "Search query")
|
|
493
635
|
.option("--limit <n>", "Max results", "5")
|
|
494
636
|
.action(async (query, opts) => {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
637
|
+
try {
|
|
638
|
+
const results = await client.search(query, parseInt(opts.limit));
|
|
639
|
+
for (const r of results) {
|
|
640
|
+
console.log(`[${r.score.toFixed(2)}] ${r.memory.content.slice(0, 80)}...`);
|
|
641
|
+
}
|
|
642
|
+
} catch (err) {
|
|
643
|
+
console.error(`Search failed: ${String(err)}`);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
mem
|
|
648
|
+
.command("delete")
|
|
649
|
+
.description("Delete a memory by ID")
|
|
650
|
+
.argument("<id>", "Memory ID")
|
|
651
|
+
.action(async (id) => {
|
|
652
|
+
try {
|
|
653
|
+
await client.delete(id);
|
|
654
|
+
console.log(`Memory ${id.slice(0, 8)}... deleted.`);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
console.error(`Delete failed: ${String(err)}`);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
mem
|
|
661
|
+
.command("export")
|
|
662
|
+
.description("Export all memories to JSON file")
|
|
663
|
+
.option("--output <path>", "Output file path", "memories-export.json")
|
|
664
|
+
.action(async (opts) => {
|
|
665
|
+
try {
|
|
666
|
+
console.log("Exporting memories...");
|
|
667
|
+
const memories = await client.export();
|
|
668
|
+
const fs = await import("fs/promises");
|
|
669
|
+
await fs.writeFile(opts.output, JSON.stringify(memories, null, 2));
|
|
670
|
+
console.log(`Exported ${memories.length} memories to ${opts.output}`);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
console.error(`Export failed: ${String(err)}`);
|
|
498
673
|
}
|
|
499
674
|
});
|
|
500
675
|
},
|
|
@@ -506,12 +681,23 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
506
681
|
// ========================================================================
|
|
507
682
|
|
|
508
683
|
// Auto-recall: inject relevant memories before agent starts
|
|
509
|
-
if (cfg
|
|
684
|
+
if (cfg?.autoRecall) {
|
|
510
685
|
api.on("before_agent_start", async (event) => {
|
|
511
686
|
if (!event.prompt || event.prompt.length < 10) {
|
|
512
687
|
return;
|
|
513
688
|
}
|
|
514
689
|
|
|
690
|
+
// NEW: Check if current channel is excluded
|
|
691
|
+
if (cfg.excludeChannels && event.channel) {
|
|
692
|
+
const channelId = String(event.channel);
|
|
693
|
+
if (cfg.excludeChannels.some((excluded) => channelId.includes(excluded))) {
|
|
694
|
+
api.logger.debug?.(
|
|
695
|
+
`memory-memoryrelay: skipping auto-recall for excluded channel: ${channelId}`,
|
|
696
|
+
);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
515
701
|
try {
|
|
516
702
|
const results = await client.search(
|
|
517
703
|
event.prompt,
|
|
@@ -523,9 +709,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
523
709
|
return;
|
|
524
710
|
}
|
|
525
711
|
|
|
526
|
-
const memoryContext = results
|
|
527
|
-
.map((r) => `- ${r.memory.content}`)
|
|
528
|
-
.join("\n");
|
|
712
|
+
const memoryContext = results.map((r) => `- ${r.memory.content}`).join("\n");
|
|
529
713
|
|
|
530
714
|
api.logger.info?.(
|
|
531
715
|
`memory-memoryrelay: injecting ${results.length} memories into context`,
|
|
@@ -541,7 +725,7 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
541
725
|
}
|
|
542
726
|
|
|
543
727
|
// Auto-capture: analyze and store important information after agent ends
|
|
544
|
-
if (cfg
|
|
728
|
+
if (cfg?.autoCapture) {
|
|
545
729
|
api.on("agent_end", async (event) => {
|
|
546
730
|
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
547
731
|
return;
|
|
@@ -596,6 +780,6 @@ export default async function plugin(api: OpenClawPluginApi): Promise<void> {
|
|
|
596
780
|
}
|
|
597
781
|
|
|
598
782
|
api.logger.info?.(
|
|
599
|
-
`memory-memoryrelay: plugin loaded (autoRecall: ${cfg
|
|
783
|
+
`memory-memoryrelay: plugin loaded (autoRecall: ${cfg?.autoRecall}, autoCapture: ${cfg?.autoCapture})`,
|
|
600
784
|
);
|
|
601
785
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
"kind": "memory",
|
|
4
4
|
"name": "MemoryRelay AI",
|
|
5
5
|
"description": "AI memory service using MemoryRelay API (api.memoryrelay.net)",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.6.2",
|
|
7
7
|
"uiHints": {
|
|
8
8
|
"apiKey": {
|
|
9
9
|
"label": "MemoryRelay API Key",
|
|
10
10
|
"sensitive": true,
|
|
11
11
|
"placeholder": "mem_prod_...",
|
|
12
|
-
"help": "API key from memoryrelay.io (or use ${MEMORYRELAY_API_KEY})"
|
|
12
|
+
"help": "API key from memoryrelay.io (or use ${MEMORYRELAY_API_KEY} env var)"
|
|
13
13
|
},
|
|
14
14
|
"agentId": {
|
|
15
15
|
"label": "Agent ID",
|
|
16
16
|
"placeholder": "iris",
|
|
17
|
-
"help": "Unique identifier for this agent in MemoryRelay"
|
|
17
|
+
"help": "Unique identifier for this agent in MemoryRelay (or use ${MEMORYRELAY_AGENT_ID} env var)"
|
|
18
18
|
},
|
|
19
19
|
"apiUrl": {
|
|
20
20
|
"label": "API URL",
|
|
21
21
|
"placeholder": "https://api.memoryrelay.net",
|
|
22
22
|
"advanced": true,
|
|
23
|
-
"help": "MemoryRelay API endpoint"
|
|
23
|
+
"help": "MemoryRelay API endpoint (or use ${MEMORYRELAY_API_URL} env var)"
|
|
24
24
|
},
|
|
25
25
|
"autoCapture": {
|
|
26
26
|
"label": "Auto-Capture",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"autoRecall": {
|
|
30
30
|
"label": "Auto-Recall",
|
|
31
31
|
"help": "Automatically inject relevant memories into context"
|
|
32
|
+
},
|
|
33
|
+
"excludeChannels": {
|
|
34
|
+
"label": "Exclude Channels",
|
|
35
|
+
"help": "List of channel IDs to skip auto-recall (e.g., ['whatsapp:group_123', 'telegram:456'])",
|
|
36
|
+
"advanced": true
|
|
32
37
|
}
|
|
33
38
|
},
|
|
34
39
|
"configSchema": {
|
|
@@ -38,12 +43,12 @@
|
|
|
38
43
|
"apiKey": {
|
|
39
44
|
"type": "string",
|
|
40
45
|
"minLength": 1,
|
|
41
|
-
"description": "MemoryRelay API key (
|
|
46
|
+
"description": "MemoryRelay API key (optional, can use MEMORYRELAY_API_KEY env var)"
|
|
42
47
|
},
|
|
43
48
|
"agentId": {
|
|
44
49
|
"type": "string",
|
|
45
50
|
"minLength": 1,
|
|
46
|
-
"description": "Unique agent identifier (
|
|
51
|
+
"description": "Unique agent identifier (optional, can use MEMORYRELAY_AGENT_ID env var or agent name)"
|
|
47
52
|
},
|
|
48
53
|
"apiUrl": {
|
|
49
54
|
"type": "string",
|
|
@@ -59,11 +64,23 @@
|
|
|
59
64
|
},
|
|
60
65
|
"recallLimit": {
|
|
61
66
|
"type": "number",
|
|
62
|
-
"default": 5
|
|
67
|
+
"default": 5,
|
|
68
|
+
"minimum": 1,
|
|
69
|
+
"maximum": 20
|
|
63
70
|
},
|
|
64
71
|
"recallThreshold": {
|
|
65
72
|
"type": "number",
|
|
66
|
-
"default": 0.3
|
|
73
|
+
"default": 0.3,
|
|
74
|
+
"minimum": 0,
|
|
75
|
+
"maximum": 1
|
|
76
|
+
},
|
|
77
|
+
"excludeChannels": {
|
|
78
|
+
"type": "array",
|
|
79
|
+
"items": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"default": [],
|
|
83
|
+
"description": "List of channel IDs to exclude from auto-recall"
|
|
67
84
|
}
|
|
68
85
|
},
|
|
69
86
|
"required": []
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memoryrelay/plugin-memoryrelay-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "OpenClaw memory plugin for MemoryRelay API - long-term memory with semantic search",
|
|
3
|
+
"version": "0.6.2",
|
|
4
|
+
"description": "OpenClaw memory plugin for MemoryRelay API - long-term memory with semantic search (ENHANCED)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"test:watch": "vitest",
|
|
10
|
+
"test:coverage": "vitest run --coverage",
|
|
11
|
+
"postinstall": "node -e \"console.log('\\n⚠️ CONFIGURATION REQUIRED\\n\\nAdd config after installation:\\n\\ncat ~/.openclaw/openclaw.json | jq \\'.plugins.entries.\\\"plugin-memoryrelay-ai\\\".config = {\\\"apiKey\\\": \\\"YOUR_KEY\\\", \\\"agentId\\\": \\\"YOUR_AGENT\\\"}\\' > /tmp/config.json && mv /tmp/config.json ~/.openclaw/openclaw.json\\n\\nOr use environment variables:\\nexport MEMORYRELAY_API_KEY=\\\"mem_prod_...\\\"\\nexport MEMORYRELAY_AGENT_ID=\\\"your-agent\\\"\\n\\nThen restart: openclaw gateway restart\\n\\nGet your API key from: https://memoryrelay.ai\\n')\""
|
|
9
12
|
},
|
|
10
13
|
"keywords": [
|
|
11
14
|
"openclaw",
|
|
@@ -30,10 +33,14 @@
|
|
|
30
33
|
"peerDependencies": {
|
|
31
34
|
"openclaw": ">=2026.2.0"
|
|
32
35
|
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"vitest": "^1.2.0",
|
|
38
|
+
"@vitest/coverage-v8": "^1.2.0"
|
|
39
|
+
},
|
|
33
40
|
"openclaw": {
|
|
34
41
|
"id": "plugin-memoryrelay-ai",
|
|
35
42
|
"extensions": [
|
|
36
|
-
"./"
|
|
43
|
+
"./index.ts"
|
|
37
44
|
]
|
|
38
45
|
},
|
|
39
46
|
"files": [
|