@mem9/mem9 0.4.1 → 0.4.3
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 +18 -17
- package/index.test.ts +193 -0
- package/index.ts +41 -9
- package/openclaw.plugin.json +7 -0
- package/package.json +3 -1
- package/server-backend.test.ts +82 -0
- package/server-backend.ts +21 -4
- package/types.ts +1 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# OpenClaw Plugin for
|
|
1
|
+
# OpenClaw Plugin for mem9
|
|
2
2
|
|
|
3
|
-
Memory plugin for [OpenClaw](https://github.com/openclaw) — replaces the built-in memory slot with cloud-persistent shared memory. Runs in server mode only, connecting to `mnemo-server` via `apiUrl` + `apiKey` (preferred) or legacy `tenantID`.
|
|
3
|
+
Memory plugin for [OpenClaw](https://github.com/openclaw) — replaces the built-in memory slot with cloud-persistent shared memory. Runs in server mode only, connecting to `mnemo-server` via `apiUrl` + `apiKey` (preferred) or legacy `tenantID`. Optional `provisionQueryParams` can forward `utm_*` attribution only during first-time auto-provisioning.
|
|
4
4
|
|
|
5
5
|
## 🚀 Quick Start (Server Mode)
|
|
6
6
|
|
|
@@ -20,14 +20,14 @@ curl -s -X POST http://localhost:8080/v1alpha1/mem9s \
|
|
|
20
20
|
# {"id": "uuid"}
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Add
|
|
23
|
+
Add mem9 to your project's `openclaw.json`:
|
|
24
24
|
|
|
25
25
|
```json
|
|
26
26
|
{
|
|
27
27
|
"plugins": {
|
|
28
|
-
"slots": { "memory": "
|
|
28
|
+
"slots": { "memory": "mem9" },
|
|
29
29
|
"entries": {
|
|
30
|
-
"
|
|
30
|
+
"mem9": {
|
|
31
31
|
"enabled": true,
|
|
32
32
|
"config": {
|
|
33
33
|
"apiUrl": "http://localhost:8080",
|
|
@@ -64,8 +64,8 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
64
64
|
|
|
65
65
|
| Hook | Trigger | What it does |
|
|
66
66
|
|---|---|---|
|
|
67
|
-
| `before_prompt_build` | Every LLM call | Searches memories by current prompt
|
|
68
|
-
| `after_compaction` | After `/compact` |
|
|
67
|
+
| `before_prompt_build` | Every LLM call | Searches memories by current prompt and injects relevant ones as context |
|
|
68
|
+
| `after_compaction` | After `/compact` | Logs compaction so the next prompt re-queries memories from the server |
|
|
69
69
|
| `before_reset` | Before `/reset` | Saves a session summary (last 3 user messages) as memory before context is wiped |
|
|
70
70
|
| `agent_end` | Agent finishes | Auto-captures the last assistant response as memory (if substantial) |
|
|
71
71
|
|
|
@@ -73,7 +73,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
73
73
|
|
|
74
74
|
| Tool | Description |
|
|
75
75
|
|---|---|
|
|
76
|
-
| `memory_store` | Store a new memory
|
|
76
|
+
| `memory_store` | Store a new memory |
|
|
77
77
|
| `memory_search` | Hybrid vector + keyword search (or keyword-only) |
|
|
78
78
|
| `memory_get` | Retrieve a single memory by ID |
|
|
79
79
|
| `memory_update` | Update an existing memory |
|
|
@@ -91,7 +91,7 @@ This is a `kind: "memory"` plugin — OpenClaw's framework manages when to load/
|
|
|
91
91
|
### Method A: npm install (Recommended)
|
|
92
92
|
|
|
93
93
|
```bash
|
|
94
|
-
openclaw plugins install @mem9/
|
|
94
|
+
openclaw plugins install @mem9/mem9
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
### Method B: From source
|
|
@@ -104,7 +104,7 @@ npm install
|
|
|
104
104
|
|
|
105
105
|
### Configure OpenClaw
|
|
106
106
|
|
|
107
|
-
Add
|
|
107
|
+
Add mem9 to your project's `openclaw.json`:
|
|
108
108
|
|
|
109
109
|
OpenClaw is often deployed across teams with multiple agents. Server mode gives you:
|
|
110
110
|
|
|
@@ -139,10 +139,10 @@ Each agent uses the same `apiKey` for the shared memory pool. The plugin sends t
|
|
|
139
139
|
{
|
|
140
140
|
"plugins": {
|
|
141
141
|
"slots": {
|
|
142
|
-
"memory": "
|
|
142
|
+
"memory": "mem9"
|
|
143
143
|
},
|
|
144
144
|
"entries": {
|
|
145
|
-
"
|
|
145
|
+
"mem9": {
|
|
146
146
|
"enabled": true,
|
|
147
147
|
"config": {
|
|
148
148
|
"apiUrl": "http://your-server:8080",
|
|
@@ -160,8 +160,8 @@ That's it. The server handles scoping and conflict resolution. Conceptually, the
|
|
|
160
160
|
|
|
161
161
|
Start OpenClaw. You should see:
|
|
162
162
|
|
|
163
|
-
```
|
|
164
|
-
[mem9] Server mode
|
|
163
|
+
```text
|
|
164
|
+
[mem9] Server mode (v1alpha2)
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
If you see `[mem9] No mode configured...`, check your `openclaw.json` config.
|
|
@@ -174,11 +174,12 @@ Defined in `openclaw.plugin.json`:
|
|
|
174
174
|
|---|---|---|
|
|
175
175
|
| `apiUrl` | string | mnemo-server URL |
|
|
176
176
|
| `apiKey` | string | Preferred key. Uses `/v1alpha2/mem9s/...` with `X-API-Key` header |
|
|
177
|
+
| `provisionQueryParams` | object | Optional `utm_*` map forwarded only to the initial `POST /v1alpha1/mem9s` auto-provision request when `apiKey` is absent |
|
|
177
178
|
| `defaultTimeoutMs` | number | Default timeout for non-search mem9 API requests in milliseconds. Default: `8000` |
|
|
178
179
|
| `searchTimeoutMs` | number | Timeout for `memory_search` and automatic recall search in milliseconds. Default: `15000` |
|
|
179
180
|
| `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
|
|
180
181
|
|
|
181
|
-
> **Note**: `apiKey` takes precedence when both fields are set. If only `tenantID` is present, the plugin treats it as a legacy alias for `apiKey`, still uses v1alpha2, and logs a deprecation warning once at startup.
|
|
182
|
+
> **Note**: `apiKey` takes precedence when both fields are set. If only `tenantID` is present, the plugin treats it as a legacy alias for `apiKey`, still uses v1alpha2, and logs a deprecation warning once at startup. `provisionQueryParams` is ignored after an `apiKey` is already configured, and non-`utm_*` keys are dropped before the provision request is sent.
|
|
182
183
|
|
|
183
184
|
## Timeout Behavior
|
|
184
185
|
|
|
@@ -213,7 +214,7 @@ Example:
|
|
|
213
214
|
openclaw-plugin/
|
|
214
215
|
├── README.md # This file
|
|
215
216
|
├── openclaw.plugin.json # Plugin metadata + config schema
|
|
216
|
-
├── package.json # npm package (@mem9/
|
|
217
|
+
├── package.json # npm package (@mem9/mem9)
|
|
217
218
|
├── index.ts # Plugin entry point + tool registration
|
|
218
219
|
├── backend.ts # MemoryBackend interface
|
|
219
220
|
├── server-backend.ts # Server mode: fetch → mnemo API
|
|
@@ -228,4 +229,4 @@ openclaw-plugin/
|
|
|
228
229
|
| `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
|
|
229
230
|
| `Server mode requires...` | Missing key | Add `apiKey` (or legacy `tenantID`) to config |
|
|
230
231
|
| Search requests time out | Hybrid/vector search exceeds plugin timeout | Increase `searchTimeoutMs` in plugin config |
|
|
231
|
-
| Plugin not loading | Not in memory slot | Set `"slots": {"memory": "
|
|
232
|
+
| Plugin not loading | Not in memory slot | Set `"slots": {"memory": "mem9"}` in openclaw.json |
|
package/index.test.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import mnemoPlugin from "./index.js";
|
|
5
|
+
|
|
6
|
+
interface StubApi {
|
|
7
|
+
pluginConfig?: unknown;
|
|
8
|
+
logger: {
|
|
9
|
+
info: (...args: unknown[]) => void;
|
|
10
|
+
error: (...args: unknown[]) => void;
|
|
11
|
+
};
|
|
12
|
+
registerTool: () => void;
|
|
13
|
+
registerCapability?: (slot: string, capability: unknown) => void;
|
|
14
|
+
on: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createStubApi(
|
|
18
|
+
pluginConfig: unknown,
|
|
19
|
+
options?: {
|
|
20
|
+
onRegisterCapability?: (slot: string, capability: unknown) => void;
|
|
21
|
+
infoLogs?: string[];
|
|
22
|
+
errorLogs?: string[];
|
|
23
|
+
},
|
|
24
|
+
): StubApi {
|
|
25
|
+
const infoLogs = options?.infoLogs ?? [];
|
|
26
|
+
const errorLogs = options?.errorLogs ?? [];
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
pluginConfig,
|
|
30
|
+
logger: {
|
|
31
|
+
info: (...args: unknown[]) => {
|
|
32
|
+
infoLogs.push(args.map(String).join(" "));
|
|
33
|
+
},
|
|
34
|
+
error: (...args: unknown[]) => {
|
|
35
|
+
errorLogs.push(args.map(String).join(" "));
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
registerTool: () => {},
|
|
39
|
+
registerCapability: options?.onRegisterCapability,
|
|
40
|
+
on: () => {},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function flushAsyncWork(): Promise<void> {
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test("register eagerly auto-provisions when apiKey is absent", async () => {
|
|
49
|
+
const originalFetch = globalThis.fetch;
|
|
50
|
+
const requests: string[] = [];
|
|
51
|
+
const infoLogs: string[] = [];
|
|
52
|
+
const errorLogs: string[] = [];
|
|
53
|
+
|
|
54
|
+
globalThis.fetch = async (input) => {
|
|
55
|
+
requests.push(String(input));
|
|
56
|
+
|
|
57
|
+
return new Response(JSON.stringify({ id: "space-1" }), {
|
|
58
|
+
status: 201,
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
mnemoPlugin.register(
|
|
67
|
+
createStubApi(
|
|
68
|
+
{
|
|
69
|
+
apiUrl: "https://api.mem9.ai",
|
|
70
|
+
provisionQueryParams: {
|
|
71
|
+
utm_source: "bosn",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{ infoLogs, errorLogs },
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await flushAsyncWork();
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(errorLogs, []);
|
|
81
|
+
assert.equal(requests.length, 1);
|
|
82
|
+
assert.equal(
|
|
83
|
+
requests[0],
|
|
84
|
+
"https://api.mem9.ai/v1alpha1/mem9s?utm_source=bosn",
|
|
85
|
+
);
|
|
86
|
+
assert.equal(
|
|
87
|
+
infoLogs.includes("[mem9] apiKey not configured; starting auto-provision"),
|
|
88
|
+
true,
|
|
89
|
+
);
|
|
90
|
+
assert.equal(
|
|
91
|
+
infoLogs.some((msg) => msg.includes("*** Auto-provisioned apiKey=space-1 ***")),
|
|
92
|
+
true,
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
globalThis.fetch = originalFetch;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("search retries auto-provision after an eager startup failure", async () => {
|
|
100
|
+
const originalFetch = globalThis.fetch;
|
|
101
|
+
const infoLogs: string[] = [];
|
|
102
|
+
const errorLogs: string[] = [];
|
|
103
|
+
let provisionAttempts = 0;
|
|
104
|
+
let searchRequests = 0;
|
|
105
|
+
let capability: {
|
|
106
|
+
search: (query: string, opts?: { limit?: number }) => Promise<{ data: unknown[]; total: number }>;
|
|
107
|
+
} | null = null;
|
|
108
|
+
|
|
109
|
+
globalThis.fetch = async (input, init) => {
|
|
110
|
+
const url = String(input);
|
|
111
|
+
|
|
112
|
+
if (url.includes("/v1alpha1/mem9s")) {
|
|
113
|
+
provisionAttempts += 1;
|
|
114
|
+
|
|
115
|
+
if (provisionAttempts === 1) {
|
|
116
|
+
return new Response(JSON.stringify({ error: "boom" }), {
|
|
117
|
+
status: 500,
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return new Response(JSON.stringify({ id: "space-2" }), {
|
|
125
|
+
status: 201,
|
|
126
|
+
headers: {
|
|
127
|
+
"Content-Type": "application/json",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (url.includes("/v1alpha2/mem9s/memories")) {
|
|
133
|
+
searchRequests += 1;
|
|
134
|
+
const headers = init?.headers as Record<string, string> | undefined;
|
|
135
|
+
assert.equal(headers?.["X-API-Key"], "space-2");
|
|
136
|
+
|
|
137
|
+
return new Response(
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
memories: [],
|
|
140
|
+
total: 0,
|
|
141
|
+
limit: 20,
|
|
142
|
+
offset: 0,
|
|
143
|
+
}),
|
|
144
|
+
{
|
|
145
|
+
status: 200,
|
|
146
|
+
headers: {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error(`unexpected fetch: ${url}`);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
mnemoPlugin.register(
|
|
158
|
+
createStubApi(
|
|
159
|
+
{
|
|
160
|
+
apiUrl: "https://api.mem9.ai",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
infoLogs,
|
|
164
|
+
errorLogs,
|
|
165
|
+
onRegisterCapability: (_slot, registeredCapability) => {
|
|
166
|
+
capability = registeredCapability as typeof capability;
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await flushAsyncWork();
|
|
173
|
+
|
|
174
|
+
assert.equal(provisionAttempts, 1);
|
|
175
|
+
assert.equal(
|
|
176
|
+
errorLogs.some((msg) => msg.includes("auto-provision failed: mem9s provision failed (500):")),
|
|
177
|
+
true,
|
|
178
|
+
);
|
|
179
|
+
assert.notEqual(capability, null);
|
|
180
|
+
|
|
181
|
+
const result = await capability!.search("hello");
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(result, { data: [], total: 0 });
|
|
184
|
+
assert.equal(provisionAttempts, 2);
|
|
185
|
+
assert.equal(searchRequests, 1);
|
|
186
|
+
assert.equal(
|
|
187
|
+
infoLogs.some((msg) => msg.includes("*** Auto-provisioned apiKey=space-2 ***")),
|
|
188
|
+
true,
|
|
189
|
+
);
|
|
190
|
+
} finally {
|
|
191
|
+
globalThis.fetch = originalFetch;
|
|
192
|
+
}
|
|
193
|
+
});
|
package/index.ts
CHANGED
|
@@ -75,6 +75,10 @@ function jsonResult(data: unknown) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function errorMessage(err: unknown): string {
|
|
79
|
+
return err instanceof Error ? err.message : String(err);
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
interface MemoryCapability {
|
|
79
83
|
search: (query: string, opts?: { limit?: number }) => Promise<{ data: Memory[]; total: number }>;
|
|
80
84
|
store: (content: string, opts?: { tags?: string[]; source?: string }) => Promise<unknown>;
|
|
@@ -303,6 +307,7 @@ const mnemoPlugin = {
|
|
|
303
307
|
const cfg = (api.pluginConfig ?? {}) as PluginConfig;
|
|
304
308
|
const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
|
|
305
309
|
const timeoutConfig = resolveTimeouts(cfg, api.logger);
|
|
310
|
+
const hookAgentId = cfg.agentName ?? "agent";
|
|
306
311
|
if (!cfg.apiUrl) {
|
|
307
312
|
api.logger.info(`[mem9] apiUrl not configured, using default ${DEFAULT_API_URL}`);
|
|
308
313
|
}
|
|
@@ -314,7 +319,15 @@ const mnemoPlugin = {
|
|
|
314
319
|
api.logger.info("[mem9] tenantID is deprecated; treating it as apiKey for v1alpha2");
|
|
315
320
|
}
|
|
316
321
|
const registerTenant = async (agentName: string): Promise<string> => {
|
|
317
|
-
const backend = new ServerBackend(
|
|
322
|
+
const backend = new ServerBackend(
|
|
323
|
+
effectiveApiUrl,
|
|
324
|
+
"",
|
|
325
|
+
agentName,
|
|
326
|
+
{
|
|
327
|
+
timeouts: timeoutConfig,
|
|
328
|
+
provisionQueryParams: cfg.provisionQueryParams ?? {},
|
|
329
|
+
},
|
|
330
|
+
);
|
|
318
331
|
const result = await backend.register();
|
|
319
332
|
api.logger.info(
|
|
320
333
|
`[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
|
|
@@ -325,14 +338,22 @@ const mnemoPlugin = {
|
|
|
325
338
|
const resolveAPIKey = (agentName: string): Promise<string> => {
|
|
326
339
|
if (configuredApiKey) return Promise.resolve(configuredApiKey);
|
|
327
340
|
if (!registrationPromise) {
|
|
328
|
-
registrationPromise = registerTenant(agentName)
|
|
341
|
+
registrationPromise = registerTenant(agentName).catch((err) => {
|
|
342
|
+
registrationPromise = null;
|
|
343
|
+
throw err;
|
|
344
|
+
});
|
|
329
345
|
}
|
|
330
346
|
return registrationPromise;
|
|
331
347
|
};
|
|
332
348
|
|
|
333
349
|
api.logger.info("[mem9] Server mode (v1alpha2)");
|
|
334
350
|
|
|
335
|
-
|
|
351
|
+
if (!configuredApiKey) {
|
|
352
|
+
api.logger.info("[mem9] apiKey not configured; starting auto-provision");
|
|
353
|
+
void resolveAPIKey(hookAgentId).catch((err) => {
|
|
354
|
+
api.logger.error(`[mem9] auto-provision failed: ${errorMessage(err)}`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
336
357
|
|
|
337
358
|
const factory: ToolFactory = (ctx: ToolContext) => {
|
|
338
359
|
const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
|
|
@@ -390,15 +411,24 @@ const toolNames = [
|
|
|
390
411
|
];
|
|
391
412
|
|
|
392
413
|
class LazyServerBackend implements MemoryBackend {
|
|
414
|
+
private apiUrl: string;
|
|
415
|
+
private apiKeyProvider: () => Promise<string>;
|
|
416
|
+
private agentId: string;
|
|
417
|
+
private timeouts: BackendTimeouts;
|
|
393
418
|
private resolved: ServerBackend | null = null;
|
|
394
419
|
private resolving: Promise<ServerBackend> | null = null;
|
|
395
420
|
|
|
396
421
|
constructor(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
) {
|
|
422
|
+
apiUrl: string,
|
|
423
|
+
apiKeyProvider: () => Promise<string>,
|
|
424
|
+
agentId: string,
|
|
425
|
+
timeouts: BackendTimeouts,
|
|
426
|
+
) {
|
|
427
|
+
this.apiUrl = apiUrl;
|
|
428
|
+
this.apiKeyProvider = apiKeyProvider;
|
|
429
|
+
this.agentId = agentId;
|
|
430
|
+
this.timeouts = timeouts;
|
|
431
|
+
}
|
|
402
432
|
|
|
403
433
|
private async resolve(): Promise<ServerBackend> {
|
|
404
434
|
if (this.resolved) return this.resolved;
|
|
@@ -406,7 +436,9 @@ class LazyServerBackend implements MemoryBackend {
|
|
|
406
436
|
|
|
407
437
|
this.resolving = this.apiKeyProvider().then((apiKey) =>
|
|
408
438
|
Promise.resolve().then(() => {
|
|
409
|
-
this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId,
|
|
439
|
+
this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, {
|
|
440
|
+
timeouts: this.timeouts,
|
|
441
|
+
});
|
|
410
442
|
return this.resolved;
|
|
411
443
|
})
|
|
412
444
|
).catch((err) => {
|
package/openclaw.plugin.json
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
"type": "string",
|
|
15
15
|
"description": "mem9 API key (secret — do not share)"
|
|
16
16
|
},
|
|
17
|
+
"provisionQueryParams": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"description": "Optional utm_* params forwarded only during first-time auto-provisioning",
|
|
20
|
+
"additionalProperties": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
17
24
|
"defaultTimeoutMs": {
|
|
18
25
|
"type": "number",
|
|
19
26
|
"minimum": 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mem9/mem9",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "OpenClaw shared memory plugin — cloud-persistent memory with hybrid vector + keyword search via mnemo-server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
+
"test": "rm -rf ./dist-test && tsc -p ./tsconfig.test.json && node --test ./dist-test/*.test.js",
|
|
37
38
|
"typecheck": "tsc --noEmit",
|
|
38
39
|
"prepublishOnly": "npm run typecheck"
|
|
39
40
|
},
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {},
|
|
44
45
|
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.15.30",
|
|
45
47
|
"typescript": "^5.5.0"
|
|
46
48
|
},
|
|
47
49
|
"openclaw": {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { ServerBackend } from "./server-backend.js";
|
|
5
|
+
|
|
6
|
+
test("register forwards only utm_* params during auto-provision", async () => {
|
|
7
|
+
const originalFetch = globalThis.fetch;
|
|
8
|
+
let requestedURL = "";
|
|
9
|
+
|
|
10
|
+
globalThis.fetch = async (input, init) => {
|
|
11
|
+
requestedURL = String(input);
|
|
12
|
+
assert.equal(init?.method, "POST");
|
|
13
|
+
|
|
14
|
+
return new Response(JSON.stringify({ id: "space-1" }), {
|
|
15
|
+
status: 201,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const backend = new ServerBackend("https://api.mem9.ai", "", "agent-1", {
|
|
24
|
+
provisionQueryParams: {
|
|
25
|
+
utm_source: "bosn",
|
|
26
|
+
foo: "bar",
|
|
27
|
+
utm_campaign: "spring",
|
|
28
|
+
utm_medium: "",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const result = await backend.register();
|
|
33
|
+
assert.equal(result.id, "space-1");
|
|
34
|
+
|
|
35
|
+
const url = new URL(requestedURL);
|
|
36
|
+
assert.equal(url.origin + url.pathname, "https://api.mem9.ai/v1alpha1/mem9s");
|
|
37
|
+
assert.equal(url.searchParams.get("utm_source"), "bosn");
|
|
38
|
+
assert.equal(url.searchParams.get("utm_campaign"), "spring");
|
|
39
|
+
assert.equal(url.searchParams.has("foo"), false);
|
|
40
|
+
assert.equal(url.searchParams.has("utm_medium"), false);
|
|
41
|
+
} finally {
|
|
42
|
+
globalThis.fetch = originalFetch;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("normal memory requests do not append provision query params", async () => {
|
|
47
|
+
const originalFetch = globalThis.fetch;
|
|
48
|
+
let requestedURL = "";
|
|
49
|
+
|
|
50
|
+
globalThis.fetch = async (input) => {
|
|
51
|
+
requestedURL = String(input);
|
|
52
|
+
|
|
53
|
+
return new Response(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
id: "mem-1",
|
|
56
|
+
content: "remember this",
|
|
57
|
+
created_at: "2026-04-05T00:00:00Z",
|
|
58
|
+
updated_at: "2026-04-05T00:00:00Z",
|
|
59
|
+
}),
|
|
60
|
+
{
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const backend = new ServerBackend("https://api.mem9.ai", "space-key", "agent-1", {
|
|
71
|
+
provisionQueryParams: {
|
|
72
|
+
utm_source: "bosn",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await backend.store({ content: "remember this" });
|
|
77
|
+
|
|
78
|
+
assert.equal(requestedURL, "https://api.mem9.ai/v1alpha2/mem9s/memories");
|
|
79
|
+
} finally {
|
|
80
|
+
globalThis.fetch = originalFetch;
|
|
81
|
+
}
|
|
82
|
+
});
|
package/server-backend.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface BackendTimeouts {
|
|
|
22
22
|
searchTimeoutMs?: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
interface ServerBackendOptions {
|
|
26
|
+
timeouts?: BackendTimeouts;
|
|
27
|
+
provisionQueryParams?: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
interface RequestOptions {
|
|
26
31
|
timeoutMs?: number;
|
|
27
32
|
}
|
|
@@ -30,25 +35,37 @@ export class ServerBackend implements MemoryBackend {
|
|
|
30
35
|
private baseUrl: string;
|
|
31
36
|
private apiKey: string;
|
|
32
37
|
private agentName: string;
|
|
38
|
+
private provisionQueryParams: Record<string, string>;
|
|
33
39
|
private timeouts: Required<BackendTimeouts>;
|
|
34
40
|
|
|
35
41
|
constructor(
|
|
36
42
|
apiUrl: string,
|
|
37
43
|
apiKey: string,
|
|
38
44
|
agentName: string,
|
|
39
|
-
|
|
45
|
+
options: ServerBackendOptions = {},
|
|
40
46
|
) {
|
|
41
47
|
this.baseUrl = apiUrl.replace(/\/+$/, "");
|
|
42
48
|
this.apiKey = apiKey;
|
|
43
49
|
this.agentName = agentName;
|
|
50
|
+
this.provisionQueryParams = options.provisionQueryParams ?? {};
|
|
44
51
|
this.timeouts = {
|
|
45
|
-
defaultTimeoutMs: timeouts
|
|
46
|
-
searchTimeoutMs: timeouts
|
|
52
|
+
defaultTimeoutMs: options.timeouts?.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
53
|
+
searchTimeoutMs: options.timeouts?.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
47
54
|
};
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
async register(): Promise<ProvisionMem9sResponse> {
|
|
51
|
-
const
|
|
58
|
+
const query = new URLSearchParams();
|
|
59
|
+
for (const [key, value] of Object.entries(this.provisionQueryParams)) {
|
|
60
|
+
if (!key.startsWith("utm_") || typeof value !== "string" || value === "") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
query.set(key, value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const qs = query.toString();
|
|
68
|
+
const resp = await fetch(this.baseUrl + "/v1alpha1/mem9s" + (qs ? `?${qs}` : ""), {
|
|
52
69
|
method: "POST",
|
|
53
70
|
signal: AbortSignal.timeout(this.timeouts.defaultTimeoutMs),
|
|
54
71
|
});
|