@mem9/mem9 0.4.0 → 0.4.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/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
  }
@@ -333,14 +338,22 @@ const mnemoPlugin = {
333
338
  const resolveAPIKey = (agentName: string): Promise<string> => {
334
339
  if (configuredApiKey) return Promise.resolve(configuredApiKey);
335
340
  if (!registrationPromise) {
336
- registrationPromise = registerTenant(agentName);
341
+ registrationPromise = registerTenant(agentName).catch((err) => {
342
+ registrationPromise = null;
343
+ throw err;
344
+ });
337
345
  }
338
346
  return registrationPromise;
339
347
  };
340
348
 
341
349
  api.logger.info("[mem9] Server mode (v1alpha2)");
342
350
 
343
- const hookAgentId = cfg.agentName ?? "agent";
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
+ }
344
357
 
345
358
  const factory: ToolFactory = (ctx: ToolContext) => {
346
359
  const agentId = ctx.agentId ?? cfg.agentName ?? "agent";
@@ -398,15 +411,24 @@ const toolNames = [
398
411
  ];
399
412
 
400
413
  class LazyServerBackend implements MemoryBackend {
414
+ private apiUrl: string;
415
+ private apiKeyProvider: () => Promise<string>;
416
+ private agentId: string;
417
+ private timeouts: BackendTimeouts;
401
418
  private resolved: ServerBackend | null = null;
402
419
  private resolving: Promise<ServerBackend> | null = null;
403
420
 
404
421
  constructor(
405
- private apiUrl: string,
406
- private apiKeyProvider: () => Promise<string>,
407
- private agentId: string,
408
- private timeouts: BackendTimeouts,
409
- ) {}
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
+ }
410
432
 
411
433
  private async resolve(): Promise<ServerBackend> {
412
434
  if (this.resolved) return this.resolved;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "scripts": {
37
- "test": "node --experimental-strip-types --test ./server-backend.test.ts",
37
+ "test": "rm -rf ./dist-test && tsc -p ./tsconfig.test.json && node --test ./dist-test/*.test.js",
38
38
  "typecheck": "tsc --noEmit",
39
39
  "prepublishOnly": "npm run typecheck"
40
40
  },
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
 
4
- import { ServerBackend } from "./server-backend.ts";
4
+ import { ServerBackend } from "./server-backend.js";
5
5
 
6
6
  test("register forwards only utm_* params during auto-provision", async () => {
7
7
  const originalFetch = globalThis.fetch;