@mem9/mem9 0.3.8 → 0.4.0

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 CHANGED
@@ -1,6 +1,6 @@
1
- # OpenClaw Plugin for mnemos
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 mnemo to your project's `openclaw.json`:
23
+ Add mem9 to your project's `openclaw.json`:
24
24
 
25
25
  ```json
26
26
  {
27
27
  "plugins": {
28
- "slots": { "memory": "openclaw" },
28
+ "slots": { "memory": "mem9" },
29
29
  "entries": {
30
- "openclaw": {
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, injects relevant ones as context (3-min TTL cache) |
68
- | `after_compaction` | After `/compact` | Invalidates cache so the next prompt gets fresh memories from the database |
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 (upsert by key) |
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/openclaw
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 mnemo to your project's `openclaw.json`:
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": "openclaw"
142
+ "memory": "mem9"
143
143
  },
144
144
  "entries": {
145
- "openclaw": {
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/openclaw)
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": "openclaw"}` in openclaw.json |
232
+ | Plugin not loading | Not in memory slot | Set `"slots": {"memory": "mem9"}` in openclaw.json |
package/index.ts CHANGED
@@ -314,7 +314,15 @@ const mnemoPlugin = {
314
314
  api.logger.info("[mem9] tenantID is deprecated; treating it as apiKey for v1alpha2");
315
315
  }
316
316
  const registerTenant = async (agentName: string): Promise<string> => {
317
- const backend = new ServerBackend(effectiveApiUrl, "", agentName, timeoutConfig);
317
+ const backend = new ServerBackend(
318
+ effectiveApiUrl,
319
+ "",
320
+ agentName,
321
+ {
322
+ timeouts: timeoutConfig,
323
+ provisionQueryParams: cfg.provisionQueryParams ?? {},
324
+ },
325
+ );
318
326
  const result = await backend.register();
319
327
  api.logger.info(
320
328
  `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
@@ -406,7 +414,9 @@ class LazyServerBackend implements MemoryBackend {
406
414
 
407
415
  this.resolving = this.apiKeyProvider().then((apiKey) =>
408
416
  Promise.resolve().then(() => {
409
- this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, this.timeouts);
417
+ this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId, {
418
+ timeouts: this.timeouts,
419
+ });
410
420
  return this.resolved;
411
421
  })
412
422
  ).catch((err) => {
@@ -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,13 +1,13 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.3.8",
3
+ "version": "0.4.0",
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",
7
7
  "author": "mem9-ai",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/mem9-ai/mem9.git",
10
+ "url": "git+https://github.com/mem9-ai/mem9.git",
11
11
  "directory": "openclaw-plugin"
12
12
  },
13
13
  "homepage": "https://github.com/mem9-ai/mem9/tree/main/openclaw-plugin#readme",
@@ -34,6 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "scripts": {
37
+ "test": "node --experimental-strip-types --test ./server-backend.test.ts",
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.ts";
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
- timeouts: BackendTimeouts = {},
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.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
46
- searchTimeoutMs: timeouts.searchTimeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
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 resp = await fetch(this.baseUrl + "/v1alpha1/mem9s", {
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
  });
package/types.ts CHANGED
@@ -2,6 +2,7 @@ export interface PluginConfig {
2
2
  // Server mode (apiUrl present → server)
3
3
  apiUrl?: string;
4
4
  apiKey?: string;
5
+ provisionQueryParams?: Record<string, string>;
5
6
  tenantID?: string;
6
7
  defaultTimeoutMs?: number;
7
8
  searchTimeoutMs?: number;