@mem9/mem9 0.3.3 → 0.3.5-rc.1

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
1
  # OpenClaw Plugin for mnemos
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` + `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`.
4
4
 
5
5
  ## 🚀 Quick Start (Server Mode)
6
6
 
@@ -17,7 +17,7 @@ curl -s -X POST http://localhost:8080/v1alpha1/mem9s \
17
17
  -d '{"name":"openclaw-tenant"}'
18
18
 
19
19
  # Response:
20
- # {"id": "uuid", "claim_url": "..."}
20
+ # {"id": "uuid"}
21
21
  ```
22
22
 
23
23
  Add mnemo to your project's `openclaw.json`:
@@ -31,7 +31,7 @@ Add mnemo to your project's `openclaw.json`:
31
31
  "enabled": true,
32
32
  "config": {
33
33
  "apiUrl": "http://localhost:8080",
34
- "tenantID": "uuid"
34
+ "apiKey": "uuid"
35
35
  }
36
36
  }
37
37
  }
@@ -41,7 +41,7 @@ Add mnemo to your project's `openclaw.json`:
41
41
 
42
42
  **That's it!** Restart OpenClaw and your agent now has persistent cloud memory.
43
43
 
44
- All memory calls use `/v1alpha1/mem9s/{tenantID}/memories/...` the tenant ID is carried in the URL path, and no special headers are required.
44
+ The plugin always uses `/v1alpha2/mem9s/memories/...` with `X-API-Key: <key>`. Legacy `tenantID` config is still supported as an alias for `apiKey`.
45
45
 
46
46
  ---
47
47
 
@@ -96,8 +96,8 @@ openclaw plugins install @mem9/openclaw
96
96
  ### Method B: From source
97
97
 
98
98
  ```bash
99
- git clone https://github.com/qiffang/mnemos.git
100
- cd mnemos/openclaw-plugin
99
+ git clone https://github.com/mem9-ai/mem9.git
100
+ cd mem9/openclaw-plugin
101
101
  npm install
102
102
  ```
103
103
 
@@ -127,12 +127,12 @@ curl -s -X POST http://localhost:8080/v1alpha1/mem9s \
127
127
  -d '{"name":"openclaw-tenant"}'
128
128
 
129
129
  # Response:
130
- # {"id": "uuid", "claim_url": "..."}
130
+ # {"id": "uuid"}
131
131
  ```
132
132
 
133
133
  **Step 3: Configure each OpenClaw instance**
134
134
 
135
- Each agent uses the same `tenantID` for the shared memory pool. The tenant ID is part of the URL path for all memory calls.
135
+ Each agent uses the same `apiKey` for the shared memory pool. The plugin sends that value in `X-API-Key` and never places it in the URL path. Legacy `tenantID` config still works as an alias for the same value.
136
136
 
137
137
  ```json
138
138
  {
@@ -145,7 +145,7 @@ Each agent uses the same `tenantID` for the shared memory pool. The tenant ID is
145
145
  "enabled": true,
146
146
  "config": {
147
147
  "apiUrl": "http://your-server:8080",
148
- "tenantID": "uuid"
148
+ "apiKey": "uuid"
149
149
  }
150
150
  }
151
151
  }
@@ -153,17 +153,17 @@ Each agent uses the same `tenantID` for the shared memory pool. The tenant ID is
153
153
  }
154
154
  ```
155
155
 
156
- That's it. The server handles scoping and conflict resolution. Conceptually, the only required values are `apiUrl` + `tenantID`.
156
+ That's it. The server handles scoping and conflict resolution. Conceptually, the only required values are `apiUrl` + `apiKey`.
157
157
 
158
158
  ### Verify
159
159
 
160
160
  Start OpenClaw. You should see:
161
161
 
162
162
  ```
163
- [mnemo] Server mode
163
+ [mem9] Server mode
164
164
  ```
165
165
 
166
- If you see `[mnemo] No mode configured...`, check your `openclaw.json` config.
166
+ If you see `[mem9] No mode configured...`, check your `openclaw.json` config.
167
167
 
168
168
  ## Config Schema
169
169
 
@@ -172,11 +172,10 @@ Defined in `openclaw.plugin.json`:
172
172
  | Field | Type | Description |
173
173
  |---|---|---|
174
174
  | `apiUrl` | string | mnemo-server URL |
175
- | `tenantID` | string | Tenant ID used in `/v1alpha1/mem9s/{tenantID}/memories` (preferred) |
176
- | `apiToken` | string | Legacy alias for `tenantID` kept for backward compatibility |
177
- | `userToken` | string | Legacy alias for `tenantID` — kept for backward compatibility |
175
+ | `apiKey` | string | Preferred key. Uses `/v1alpha2/mem9s/...` with `X-API-Key` header |
176
+ | `tenantID` | string | Legacy alias for `apiKey`. The plugin still uses `/v1alpha2/mem9s/...` with `X-API-Key`. |
178
177
 
179
- > **Note**: `tenantID` is the preferred config field. For legacy setups, the plugin checks `tenantID` first, then falls back to `apiToken`, then `userToken`.
178
+ > **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.
180
179
 
181
180
  ## File Structure
182
181
 
@@ -196,6 +195,6 @@ openclaw-plugin/
196
195
 
197
196
  | Problem | Cause | Fix |
198
197
  |---|---|---|
199
- | `No mode configured` | Missing config | Add `apiUrl` and `tenantID` (or legacy `apiToken`/`userToken`) to plugin config |
200
- | `Server mode requires...` | Missing tenant ID | Add `tenantID` (or legacy `apiToken`/`userToken`) to config |
198
+ | `No mode configured` | Missing config | Add `apiUrl` and `apiKey` (or legacy `tenantID`) to plugin config |
199
+ | `Server mode requires...` | Missing key | Add `apiKey` (or legacy `tenantID`) to config |
201
200
  | Plugin not loading | Not in memory slot | Set `"slots": {"memory": "openclaw"}` in openclaw.json |
package/backend.ts CHANGED
@@ -21,7 +21,7 @@ export interface MemoryBackend {
21
21
 
22
22
  /**
23
23
  * Ingest messages into the smart memory pipeline.
24
- * POST /v1alpha1/mem9s/{tenantID}/memories (messages body) → LLM extraction + reconciliation.
24
+ * POST /v1alpha{1,2}/mem9s/.../memories (messages body) → LLM extraction + reconciliation.
25
25
  */
26
26
  ingest(input: IngestInput): Promise<IngestResult>;
27
27
  }
package/hooks.ts CHANGED
@@ -193,14 +193,14 @@ export function registerHooks(
193
193
 
194
194
  if (memories.length === 0) return;
195
195
 
196
- logger.info(`[mnemo] Injecting ${memories.length} memories into prompt context`);
196
+ logger.info(`[mem9] Injecting ${memories.length} memories into prompt context`);
197
197
 
198
198
  return {
199
199
  prependContext: formatMemoriesBlock(memories),
200
200
  };
201
201
  } catch (err) {
202
202
  // Graceful degradation — never block the LLM call
203
- logger.error(`[mnemo] before_prompt_build failed: ${String(err)}`);
203
+ logger.error(`[mem9] before_prompt_build failed: ${String(err)}`);
204
204
  }
205
205
  },
206
206
  { priority: 50 }, // Run after most plugins but before agent start
@@ -210,7 +210,7 @@ export function registerHooks(
210
210
  // after_compaction — no-op placeholder (no client-side cache to invalidate)
211
211
  // --------------------------------------------------------------------------
212
212
  api.on("after_compaction", async (_event: unknown) => {
213
- logger.info("[mnemo] Compaction detected — memories will be re-queried on next prompt");
213
+ logger.info("[mem9] Compaction detected — memories will be re-queried on next prompt");
214
214
  });
215
215
 
216
216
  // --------------------------------------------------------------------------
@@ -247,10 +247,10 @@ export function registerHooks(
247
247
  tags: ["auto-capture", "session-summary", "pre-reset"],
248
248
  });
249
249
 
250
- logger.info("[mnemo] Session context saved before reset");
250
+ logger.info("[mem9] Session context saved before reset");
251
251
  } catch (err) {
252
252
  // Best-effort — never block /reset
253
- logger.error(`[mnemo] before_reset save failed: ${String(err)}`);
253
+ logger.error(`[mem9] before_reset save failed: ${String(err)}`);
254
254
  }
255
255
  });
256
256
 
@@ -330,10 +330,10 @@ export function registerHooks(
330
330
 
331
331
 
332
332
  if (result.status === "accepted") {
333
- logger.info("[mnemo] Ingest accepted for async processing");
333
+ logger.info("[mem9] Ingest accepted for async processing");
334
334
  } else if ((result.memories_changed ?? 0) > 0) {
335
335
  logger.info(
336
- `[mnemo] Ingested session: memories_changed=${result.memories_changed}, status=${result.status}`
336
+ `[mem9] Ingested session: memories_changed=${result.memories_changed}, status=${result.status}`
337
337
  );
338
338
  }
339
339
  } catch {
package/index.ts CHANGED
@@ -13,7 +13,17 @@ import type {
13
13
  const DEFAULT_API_URL = "https://api.mem9.ai";
14
14
 
15
15
  function jsonResult(data: unknown) {
16
- return data;
16
+ // Older OpenClaw versions may assume tool results have a normalized
17
+ // assistant-content shape and can crash on plain objects that omit `content`.
18
+ // Returning a JSON string keeps results readable while remaining compatible
19
+ // with both old and new hosts.
20
+ // https://github.com/openclaw/openclaw/blob/936607ca221a2f0c37ad976ddefcd39596f54793/CHANGELOG.md?plain=1#L1144
21
+ if (typeof data === "string") return data;
22
+ try {
23
+ return JSON.stringify(data, null, 2);
24
+ } catch {
25
+ return String(data);
26
+ }
17
27
  }
18
28
 
19
29
  interface OpenClawPluginApi {
@@ -232,39 +242,39 @@ const mnemoPlugin = {
232
242
  const cfg = (api.pluginConfig ?? {}) as PluginConfig;
233
243
  const effectiveApiUrl = cfg.apiUrl ?? DEFAULT_API_URL;
234
244
  if (!cfg.apiUrl) {
235
- api.logger.info(`[mnemo] apiUrl not configured, using default ${DEFAULT_API_URL}`);
245
+ api.logger.info(`[mem9] apiUrl not configured, using default ${DEFAULT_API_URL}`);
236
246
  }
237
247
 
238
-
239
- const configuredTenantID = cfg.tenantID;
248
+ const configuredApiKey = cfg.apiKey ?? cfg.tenantID;
249
+ if (cfg.apiKey && cfg.tenantID) {
250
+ api.logger.info("[mem9] both apiKey and tenantID set; using apiKey");
251
+ } else if (cfg.tenantID) {
252
+ api.logger.info("[mem9] tenantID is deprecated; treating it as apiKey for v1alpha2");
253
+ }
240
254
  const registerTenant = async (agentName: string): Promise<string> => {
241
255
  const backend = new ServerBackend(effectiveApiUrl, "", agentName);
242
256
  const result = await backend.register();
243
- const claimUrl = result.claim_url ?? "(not provided)";
244
- api.logger.info(
245
- `[mnemo] *** Auto-provisioned tenant_id=${result.id} *** Save this tenant ID to your config as tenantID`
246
- );
247
257
  api.logger.info(
248
- `[mnemo] Claim your TiDB instance at: ${claimUrl}`
258
+ `[mem9] *** Auto-provisioned apiKey=${result.id} *** Save this to your config as apiKey`
249
259
  );
250
260
  return result.id;
251
261
  };
252
262
  let registrationPromise: Promise<string> | null = null;
253
- const resolveTenantID = (agentName: string): Promise<string> => {
254
- if (configuredTenantID) return Promise.resolve(configuredTenantID);
263
+ const resolveAPIKey = (agentName: string): Promise<string> => {
264
+ if (configuredApiKey) return Promise.resolve(configuredApiKey);
255
265
  if (!registrationPromise) {
256
266
  registrationPromise = registerTenant(agentName);
257
267
  }
258
268
  return registrationPromise;
259
269
  };
260
270
 
261
- api.logger.info("[mnemo] Server mode (tenant-scoped mem9 API)");
271
+ api.logger.info("[mem9] Server mode (v1alpha2)");
262
272
 
263
273
  const factory: ToolFactory = (ctx: ToolContext) => {
264
274
  const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
265
275
  const backend = new LazyServerBackend(
266
276
  effectiveApiUrl,
267
- () => resolveTenantID(agentId),
277
+ () => resolveAPIKey(agentId),
268
278
  agentId,
269
279
  );
270
280
  return buildTools(backend);
@@ -276,7 +286,7 @@ const mnemoPlugin = {
276
286
  // Uses the default workspace/agent context for hook-triggered operations.
277
287
  const hookBackend = new LazyServerBackend(
278
288
  effectiveApiUrl,
279
- () => resolveTenantID(cfg.agentName ?? "agent"),
289
+ () => resolveAPIKey(cfg.agentName ?? "agent"),
280
290
  cfg.agentName ?? "agent",
281
291
  );
282
292
  registerHooks(api, hookBackend, api.logger, { maxIngestBytes: cfg.maxIngestBytes });
@@ -297,7 +307,7 @@ class LazyServerBackend implements MemoryBackend {
297
307
 
298
308
  constructor(
299
309
  private apiUrl: string,
300
- private tenantIDProvider: () => Promise<string>,
310
+ private apiKeyProvider: () => Promise<string>,
301
311
  private agentId: string,
302
312
  ) {}
303
313
 
@@ -305,9 +315,9 @@ class LazyServerBackend implements MemoryBackend {
305
315
  if (this.resolved) return this.resolved;
306
316
  if (this.resolving) return this.resolving;
307
317
 
308
- this.resolving = this.tenantIDProvider().then((tenantID) =>
318
+ this.resolving = this.apiKeyProvider().then((apiKey) =>
309
319
  Promise.resolve().then(() => {
310
- this.resolved = new ServerBackend(this.apiUrl, tenantID, this.agentId);
320
+ this.resolved = new ServerBackend(this.apiUrl, apiKey, this.agentId);
311
321
  return this.resolved;
312
322
  })
313
323
  ).catch((err) => {
@@ -10,17 +10,13 @@
10
10
  "type": "string",
11
11
  "description": "mnemo-server URL (server mode)"
12
12
  },
13
- "tenantID": {
14
- "type": "string",
15
- "description": "Tenant ID for v1alpha1 mem9s routes (preferred)"
16
- },
17
- "apiToken": {
13
+ "apiKey": {
18
14
  "type": "string",
19
- "description": "Legacy alias for tenantID (kept for backward compatibility)"
15
+ "description": "mem9 API key (secret do not share)"
20
16
  },
21
- "userToken": {
17
+ "tenantID": {
22
18
  "type": "string",
23
- "description": "Legacy alias for tenantID"
19
+ "description": "Deprecated: use apiKey"
24
20
  }
25
21
  }
26
22
  },
@@ -29,17 +25,13 @@
29
25
  "label": "Server URL",
30
26
  "placeholder": "https://your-server.example.com"
31
27
  },
32
- "tenantID": {
33
- "label": "Tenant ID",
34
- "placeholder": "uuid..."
35
- },
36
- "apiToken": {
37
- "label": "Legacy Tenant ID Alias",
38
- "placeholder": "uuid...",
28
+ "apiKey": {
29
+ "label": "API Key",
30
+ "placeholder": "key...",
39
31
  "sensitive": true
40
32
  },
41
- "userToken": {
42
- "label": "Legacy Tenant ID Alias",
33
+ "tenantID": {
34
+ "label": "Tenant ID (legacy)",
43
35
  "placeholder": "uuid...",
44
36
  "sensitive": true
45
37
  }
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.3.3",
3
+ "version": "0.3.5-rc.1",
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
- "author": "qiffang",
7
+ "author": "mem9-ai",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/qiffang/mnemos.git",
10
+ "url": "https://github.com/mem9-ai/mem9.git",
11
11
  "directory": "openclaw-plugin"
12
12
  },
13
- "homepage": "https://github.com/qiffang/mnemos/tree/main/openclaw-plugin#readme",
13
+ "homepage": "https://github.com/mem9-ai/mem9/tree/main/openclaw-plugin#readme",
14
14
  "keywords": [
15
15
  "openclaw",
16
16
  "openclaw-plugin",
package/server-backend.ts CHANGED
@@ -12,17 +12,20 @@ import type {
12
12
 
13
13
  type ProvisionMem9sResponse = {
14
14
  id: string;
15
- claim_url?: string;
16
15
  };
17
16
 
18
17
  export class ServerBackend implements MemoryBackend {
19
18
  private baseUrl: string;
20
- private tenantID: string;
19
+ private apiKey: string;
21
20
  private agentName: string;
22
21
 
23
- constructor(apiUrl: string, tenantID: string, agentName: string) {
22
+ constructor(
23
+ apiUrl: string,
24
+ apiKey: string,
25
+ agentName: string,
26
+ ) {
24
27
  this.baseUrl = apiUrl.replace(/\/+$/, "");
25
- this.tenantID = tenantID;
28
+ this.apiKey = apiKey;
26
29
  this.agentName = agentName;
27
30
  }
28
31
 
@@ -39,22 +42,22 @@ export class ServerBackend implements MemoryBackend {
39
42
 
40
43
  const data = (await resp.json()) as ProvisionMem9sResponse;
41
44
  if (!data?.id) {
42
- throw new Error("mem9s provision did not return tenant ID");
45
+ throw new Error("mem9s provision did not return API key");
43
46
  }
44
47
 
45
- this.tenantID = data.id;
48
+ this.apiKey = data.id;
46
49
  return data;
47
50
  }
48
51
 
49
- private tenantPath(path: string): string {
50
- if (!this.tenantID) {
51
- throw new Error("tenant ID is not configured");
52
+ private memoryPath(path: string): string {
53
+ if (!this.apiKey) {
54
+ throw new Error("API key is not configured");
52
55
  }
53
- return `/v1alpha1/mem9s/${this.tenantID}${path}`;
56
+ return `/v1alpha2/mem9s${path}`;
54
57
  }
55
58
 
56
59
  async store(input: CreateMemoryInput): Promise<StoreResult> {
57
- return this.request<StoreResult>("POST", this.tenantPath("/memories"), input);
60
+ return this.request<StoreResult>("POST", this.memoryPath("/memories"), input);
58
61
  }
59
62
 
60
63
  async search(input: SearchInput): Promise<SearchResult> {
@@ -71,7 +74,7 @@ export class ServerBackend implements MemoryBackend {
71
74
  total: number;
72
75
  limit: number;
73
76
  offset: number;
74
- }>("GET", `${this.tenantPath("/memories")}${qs ? "?" + qs : ""}`);
77
+ }>("GET", `${this.memoryPath("/memories")}${qs ? "?" + qs : ""}`);
75
78
  return {
76
79
  data: raw.memories ?? [],
77
80
  total: raw.total,
@@ -82,7 +85,7 @@ export class ServerBackend implements MemoryBackend {
82
85
 
83
86
  async get(id: string): Promise<Memory | null> {
84
87
  try {
85
- return await this.request<Memory>("GET", this.tenantPath(`/memories/${id}`));
88
+ return await this.request<Memory>("GET", this.memoryPath(`/memories/${id}`));
86
89
  } catch {
87
90
  return null;
88
91
  }
@@ -90,7 +93,7 @@ export class ServerBackend implements MemoryBackend {
90
93
 
91
94
  async update(id: string, input: UpdateMemoryInput): Promise<Memory | null> {
92
95
  try {
93
- return await this.request<Memory>("PUT", this.tenantPath(`/memories/${id}`), input);
96
+ return await this.request<Memory>("PUT", this.memoryPath(`/memories/${id}`), input);
94
97
  } catch {
95
98
  return null;
96
99
  }
@@ -98,7 +101,7 @@ export class ServerBackend implements MemoryBackend {
98
101
 
99
102
  async remove(id: string): Promise<boolean> {
100
103
  try {
101
- await this.request("DELETE", this.tenantPath(`/memories/${id}`));
104
+ await this.request("DELETE", this.memoryPath(`/memories/${id}`));
102
105
  return true;
103
106
  } catch {
104
107
  return false;
@@ -106,7 +109,7 @@ export class ServerBackend implements MemoryBackend {
106
109
  }
107
110
 
108
111
  async ingest(input: IngestInput): Promise<IngestResult> {
109
- return this.request<IngestResult>("POST", this.tenantPath("/memories"), input);
112
+ return this.request<IngestResult>("POST", this.memoryPath("/memories"), input);
110
113
  }
111
114
 
112
115
  private async requestRaw(
@@ -118,6 +121,7 @@ export class ServerBackend implements MemoryBackend {
118
121
  const headers: Record<string, string> = {
119
122
  "Content-Type": "application/json",
120
123
  "X-Mnemo-Agent-Id": this.agentName,
124
+ "X-API-Key": this.apiKey,
121
125
  };
122
126
  return fetch(url, {
123
127
  method,
package/types.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  export interface PluginConfig {
2
2
  // Server mode (apiUrl present → server)
3
3
  apiUrl?: string;
4
+ apiKey?: string;
4
5
  tenantID?: string;
5
- apiToken?: string;
6
- userToken?: string;
7
6
 
8
7
  tenantName?: string;
9
8