memorylake-openclaw 0.0.2 → 0.0.4

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.
@@ -0,0 +1,23 @@
1
+ name: Publish Package to npmjs
2
+ on:
3
+ # Trigger on tag push
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v5
15
+ # Setup .npmrc file to publish to npm
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '24.x'
19
+ registry-url: 'https://registry.npmjs.org'
20
+ - run: npm i && npm ci
21
+ - run: npm publish
22
+ env:
23
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -10,7 +10,7 @@ Your agent forgets everything between sessions. This plugin fixes that. It watch
10
10
  <img src="../docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
11
11
  </p> -->
12
12
 
13
- **Auto-Recall** — Before the agent responds, the plugin searches MemoryLake for memories that match the current message and injects them into context.
13
+ **Auto-Recall** — Before the agent responds, the plugin searches MemoryLake for memories and relevant document excerpts that match the current message and injects them into context.
14
14
 
15
15
  **Auto-Capture** — After the agent responds, the plugin sends the exchange to MemoryLake. MemoryLake decides what's worth keeping — new facts get stored, stale ones updated, duplicates merged.
16
16
 
@@ -29,16 +29,15 @@ Get an API key from [app.memorylake.ai](https://app.memorylake.ai), then add to
29
29
  "memorylake-openclaw": {
30
30
  "enabled": true,
31
31
  "config": {
32
- "apiKey": "${MEMORYLAKE_API_KEY}",
33
- "projectId": "proj-...",
34
- "userId": "your-user-id"
32
+ "apiKey": "sk-...",
33
+ "projectId": "proj-..."
35
34
  }
36
35
  }
37
36
  ```
38
37
 
39
38
  ## Agent tools
40
39
 
41
- The agent gets five tools it can call during conversations:
40
+ The agent gets six tools it can call during conversations:
42
41
 
43
42
  | Tool | Description |
44
43
  |------|-------------|
@@ -47,6 +46,7 @@ The agent gets five tools it can call during conversations:
47
46
  | `memory_store` | Explicitly save a fact |
48
47
  | `memory_get` | Retrieve a memory by ID |
49
48
  | `memory_forget` | Delete a memory by ID |
49
+ | `document_search` | Search project documents for relevant paragraphs, tables, and figures |
50
50
 
51
51
  ## CLI
52
52
 
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: OpenClaw
3
+ ---
4
+
5
+ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents with the `memorylake-openclaw` plugin. Your agent forgets everything between sessions — this plugin fixes that by automatically watching conversations, extracting what matters, and bringing it back when relevant.
6
+
7
+ ## Overview
8
+
9
+ {/*<Frame>
10
+ <img src="/images/openclaw-architecture.png" alt="OpenClaw MemoryLake Architecture" />
11
+ </Frame>*/}
12
+
13
+ The plugin provides:
14
+ 1. **Auto-Recall** — Before the agent responds, memories and relevant document excerpts matching the current message are injected into context
15
+ 2. **Auto-Capture** — After the agent responds, the exchange is sent to MemoryLake which decides what's worth keeping
16
+ 3. **Agent Tools** — Six tools for memory and document operations during conversations
17
+
18
+ Both auto-recall and auto-capture run silently with no manual configuration required.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ openclaw plugins install memorylake-openclaw
24
+ ```
25
+
26
+ ## Setup and Configuration
27
+
28
+ <Note>Get your API key and project ID from [app.memorylake.ai](https://app.memorylake.ai).</Note>
29
+
30
+ Add to your `openclaw.json`:
31
+
32
+ ```json5
33
+ // plugins.entries
34
+ "memorylake-openclaw": {
35
+ "enabled": true,
36
+ "config": {
37
+ "apiKey": "sk-...",
38
+ "projectId": "proj-..."
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## Agent Tools
44
+
45
+ The agent gets six tools it can call during conversations:
46
+
47
+ | Tool | Description |
48
+ |------|-------------|
49
+ | `memory_search` | Search memories by natural language |
50
+ | `memory_list` | List all stored memories for a user |
51
+ | `memory_store` | Explicitly save a fact |
52
+ | `memory_get` | Retrieve a memory by ID |
53
+ | `memory_forget` | Delete a memory by ID |
54
+ | `document_search` | Search project documents for relevant paragraphs, tables, and figures |
55
+
56
+ ## CLI Commands
57
+
58
+ ```bash
59
+ # Search memories
60
+ openclaw memorylake search "what languages does the user know"
61
+
62
+ # View stats
63
+ openclaw memorylake stats
64
+ ```
65
+
66
+ ## Configuration Options
67
+
68
+ | Key | Type | Default | Description |
69
+ |-----|------|---------|-------------|
70
+ | `apiKey` | `string` | — | **Required.** MemoryLake API key (supports `${MEMORYLAKE_API_KEY}`) |
71
+ | `projectId` | `string` | — | **Required.** MemoryLake project ID |
72
+ | `host` | `string` | `https://app.memorylake.ai` | MemoryLake server endpoint URL |
73
+ | `userId` | `string` | `"default"` | Scope memories per user |
74
+ | `autoRecall` | `boolean` | `true` | Inject memories before each turn |
75
+ | `autoCapture` | `boolean` | `true` | Store facts after each turn |
76
+ | `topK` | `number` | `5` | Max memories per recall |
77
+ | `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
78
+ | `rerank` | `boolean` | `true` | Rerank search results for better relevance |
79
+
80
+ ## Key Features
81
+
82
+ 1. **Zero Configuration** — Auto-recall and auto-capture work out of the box with no prompting required
83
+ 2. **Async Processing** — Memory extraction runs asynchronously via MemoryLake's API
84
+ 3. **Session Tracking** — Conversations are tagged with `chat_session_id` for traceability
85
+ 4. **Rich Tool Suite** — Six agent tools for memory and document operations when needed
86
+
87
+ ## Conclusion
88
+
89
+ The `memorylake-openclaw` plugin gives OpenClaw agents persistent memory with minimal setup. Your agents can remember user preferences, facts, and context across sessions automatically.
90
+
91
+ {/*<CardGroup cols={2}>
92
+ <Card title="MemoryLake" icon="brain" href="https://app.memorylake.ai">
93
+ MemoryLake platform
94
+ </Card>
95
+ <Card title="OpenClaw" icon="robot" href="https://github.com/openclaw/openclaw">
96
+ OpenClaw agent framework
97
+ </Card>
98
+ </CardGroup>*/}
package/index.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Long-term memory via MemoryLake platform.
5
5
  *
6
6
  * Features:
7
- * - 5 tools: memory_search, memory_list, memory_store, memory_get, memory_forget
8
- * - Auto-recall: injects relevant memories (both scopes) before each agent turn
7
+ * - 6 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search
8
+ * - Auto-recall: injects relevant memories and document excerpts before each agent turn
9
9
  * - Auto-capture: stores key facts scoped to the current session after each agent turn
10
10
  * - CLI: openclaw memorylake search, openclaw memorylake stats
11
11
  */
@@ -69,6 +69,35 @@ interface AddResult {
69
69
  results: AddResultItem[];
70
70
  }
71
71
 
72
+ interface DocumentSearchResult {
73
+ type: "table" | "paragraph" | "figure";
74
+ document_id?: string;
75
+ document_name?: string;
76
+ source_document?: { file_name?: string };
77
+ highlight?: {
78
+ chunks?: Array<{ text?: string; range?: string }>;
79
+ inner_tables?: Array<{
80
+ id?: string;
81
+ columns?: Array<{ name?: string; data_type?: string }>;
82
+ num_rows?: number;
83
+ }>;
84
+ figure?: {
85
+ text?: string;
86
+ caption?: string;
87
+ summary_text?: string;
88
+ };
89
+ };
90
+ title?: string;
91
+ footnote?: string;
92
+ sheet_name?: string;
93
+ figure_id?: number;
94
+ }
95
+
96
+ interface DocumentSearchResponse {
97
+ count: number;
98
+ results: DocumentSearchResult[];
99
+ }
100
+
72
101
  // ============================================================================
73
102
  // Unified Provider Interface
74
103
  // ============================================================================
@@ -82,6 +111,7 @@ interface MemoryLakeProvider {
82
111
  get(memoryId: string): Promise<MemoryItem>;
83
112
  getAll(options: ListOptions): Promise<MemoryItem[]>;
84
113
  delete(memoryId: string): Promise<void>;
114
+ searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse>;
85
115
  }
86
116
 
87
117
  // ============================================================================
@@ -98,9 +128,11 @@ interface ApiResponse<T = unknown> {
98
128
  class PlatformProvider implements MemoryLakeProvider {
99
129
  private readonly http: ReturnType<typeof got.extend>;
100
130
  private readonly basePath: string;
131
+ private readonly docSearchPath: string;
101
132
 
102
133
  constructor(host: string, apiKey: string, projectId: string) {
103
134
  this.basePath = `openapi/memorylake/api/v2/projects/${projectId}/memories`;
135
+ this.docSearchPath = `openapi/memorylake/api/v1/projects/${projectId}/documents/search`;
104
136
  this.http = got.extend({
105
137
  prefixUrl: host,
106
138
  headers: {
@@ -175,6 +207,18 @@ class PlatformProvider implements MemoryLakeProvider {
175
207
  .json<ApiResponse>();
176
208
  if (!resp.success) throw new Error(resp.message ?? "delete failed");
177
209
  }
210
+
211
+ async searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse> {
212
+ const resp = await this.http
213
+ .post(this.docSearchPath, { json: { query, topN } })
214
+ .json<ApiResponse>();
215
+ if (!resp.success) throw new Error(resp.message ?? "document search failed");
216
+ const data = resp.data as any;
217
+ return {
218
+ count: data?.count ?? 0,
219
+ results: Array.isArray(data?.results) ? data.results : [],
220
+ };
221
+ }
178
222
  }
179
223
 
180
224
  // ============================================================================
@@ -210,19 +254,57 @@ function normalizeAddResult(raw: any): AddResult {
210
254
  }
211
255
 
212
256
  // ============================================================================
213
- // Config Parser
257
+ // Document Context Builder
214
258
  // ============================================================================
215
259
 
216
- function resolveEnvVars(value: string): string {
217
- return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
218
- const envValue = process.env[envVar];
219
- if (!envValue) {
220
- throw new Error(`Environment variable ${envVar} is not set`);
260
+ function buildDocumentContext(
261
+ results: DocumentSearchResult[],
262
+ maxChunkLength = 10000,
263
+ ): string {
264
+ const parts: string[] = [];
265
+
266
+ for (const result of results) {
267
+ const source = result.document_name ?? result.source_document?.file_name ?? "unknown";
268
+ const highlight = result.highlight;
269
+
270
+ if (result.type === "table") {
271
+ const title = result.title || "Untitled Table";
272
+ parts.push(`### Table: ${title} (from ${source})`);
273
+ if (result.footnote) parts.push(`Note: ${result.footnote}`);
274
+
275
+ for (const innerTable of highlight?.inner_tables ?? []) {
276
+ const colDesc = (innerTable.columns ?? [])
277
+ .map((c) => `${c.name}(${c.data_type})`)
278
+ .join(", ");
279
+ if (colDesc) parts.push(`Columns: ${colDesc}`);
280
+ if (innerTable.num_rows != null) parts.push(`Rows: ${innerTable.num_rows}`);
281
+ }
282
+ for (const chunk of highlight?.chunks ?? []) {
283
+ if (chunk.text) parts.push(chunk.text.slice(0, maxChunkLength));
284
+ }
285
+ } else if (result.type === "paragraph") {
286
+ parts.push(`### Paragraph (from ${source}):`);
287
+ for (const chunk of highlight?.chunks ?? []) {
288
+ if (chunk.text) parts.push(chunk.text.slice(0, maxChunkLength));
289
+ }
290
+ } else if (result.type === "figure") {
291
+ const figure = highlight?.figure;
292
+ if (figure) {
293
+ parts.push(`### Figure (from ${source}):`);
294
+ if (figure.caption) parts.push(`Caption: ${figure.caption}`);
295
+ const text = figure.text || figure.summary_text || "";
296
+ if (text) parts.push(text);
297
+ }
221
298
  }
222
- return envValue;
223
- });
299
+ }
300
+
301
+ return parts.join("\n\n");
224
302
  }
225
303
 
304
+ // ============================================================================
305
+ // Config Parser
306
+ // ============================================================================
307
+
226
308
  // ============================================================================
227
309
  // Config Schema
228
310
  // ============================================================================
@@ -267,10 +349,10 @@ const memoryLakeConfigSchema = {
267
349
  return {
268
350
  host:
269
351
  typeof cfg.host === "string" && cfg.host
270
- ? resolveEnvVars(cfg.host)
352
+ ? cfg.host
271
353
  : "https://app.memorylake.ai",
272
- apiKey: resolveEnvVars(cfg.apiKey),
273
- projectId: resolveEnvVars(cfg.projectId),
354
+ apiKey: cfg.apiKey as string,
355
+ projectId: cfg.projectId as string,
274
356
  userId:
275
357
  typeof cfg.userId === "string" && cfg.userId ? cfg.userId : "default",
276
358
  autoCapture: cfg.autoCapture !== false,
@@ -641,6 +723,66 @@ const memoryPlugin = {
641
723
  { name: "memory_forget" },
642
724
  );
643
725
 
726
+ api.registerTool(
727
+ {
728
+ name: "document_search",
729
+ label: "Document Search",
730
+ description:
731
+ "Search through documents stored in MemoryLake project. Returns relevant paragraphs, tables, and figures from uploaded documents.",
732
+ parameters: Type.Object({
733
+ query: Type.String({ description: "Search query" }),
734
+ topN: Type.Optional(
735
+ Type.Number({
736
+ description: `Max results (default: ${cfg.topK})`,
737
+ minimum: 1,
738
+ }),
739
+ ),
740
+ }),
741
+ async execute(_toolCallId, params) {
742
+ const { query, topN } = params as { query: string; topN?: number };
743
+
744
+ try {
745
+ const response = await provider.searchDocuments(
746
+ query,
747
+ topN ?? cfg.topK,
748
+ );
749
+
750
+ if (!response.results || response.results.length === 0) {
751
+ return {
752
+ content: [
753
+ { type: "text", text: "No relevant documents found." },
754
+ ],
755
+ details: { count: 0 },
756
+ };
757
+ }
758
+
759
+ const context = buildDocumentContext(response.results);
760
+
761
+ return {
762
+ content: [
763
+ {
764
+ type: "text",
765
+ text: `Found ${response.results.length} document results:\n\n${context}`,
766
+ },
767
+ ],
768
+ details: { count: response.results.length },
769
+ };
770
+ } catch (err) {
771
+ return {
772
+ content: [
773
+ {
774
+ type: "text",
775
+ text: `Document search failed: ${String(err)}`,
776
+ },
777
+ ],
778
+ details: { error: String(err) },
779
+ };
780
+ }
781
+ },
782
+ },
783
+ { name: "document_search" },
784
+ );
785
+
644
786
  // ========================================================================
645
787
  // CLI Commands
646
788
  // ========================================================================
@@ -708,7 +850,7 @@ const memoryPlugin = {
708
850
  // Lifecycle Hooks
709
851
  // ========================================================================
710
852
 
711
- // Auto-recall: inject relevant memories before agent starts
853
+ // Auto-recall: inject relevant memories and documents before agent starts
712
854
  if (cfg.autoRecall) {
713
855
  api.on("before_agent_start", async (event, ctx) => {
714
856
  if (!event.prompt || event.prompt.length < 5) return;
@@ -717,28 +859,42 @@ const memoryPlugin = {
717
859
  const sessionId = (ctx as any)?.sessionKey ?? undefined;
718
860
  if (sessionId) currentSessionId = sessionId;
719
861
 
720
- try {
721
- const results = await provider.search(
722
- event.prompt,
723
- buildSearchOptions(),
724
- );
862
+ const [memoryResult, docResult] = await Promise.allSettled([
863
+ provider.search(event.prompt, buildSearchOptions()),
864
+ provider.searchDocuments(event.prompt, cfg.topK),
865
+ ]);
725
866
 
726
- if (results.length === 0) return;
867
+ const contextParts: string[] = [];
727
868
 
728
- const memoryContext = results
869
+ if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
870
+ const memoryContext = memoryResult.value
729
871
  .map((r) => `- ${r.content}`)
730
872
  .join("\n");
731
-
873
+ contextParts.push(
874
+ `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
875
+ );
732
876
  api.logger.info(
733
- `memorylake-openclaw: injecting ${results.length} memories into context`,
877
+ `memorylake-openclaw: injecting ${memoryResult.value.length} memories into context`,
734
878
  );
879
+ } else if (memoryResult.status === "rejected") {
880
+ api.logger.warn(`memorylake-openclaw: memory recall failed: ${String(memoryResult.reason)}`);
881
+ }
735
882
 
736
- return {
737
- prependContext: `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
738
- };
739
- } catch (err) {
740
- api.logger.warn(`memorylake-openclaw: recall failed: ${String(err)}`);
883
+ if (docResult.status === "fulfilled" && docResult.value.results.length > 0) {
884
+ const docContext = buildDocumentContext(docResult.value.results);
885
+ contextParts.push(
886
+ `<relevant-documents>\nThe following document excerpts may be relevant to this conversation:\n${docContext}\n</relevant-documents>`,
887
+ );
888
+ api.logger.info(
889
+ `memorylake-openclaw: injecting ${docResult.value.results.length} document results into context`,
890
+ );
891
+ } else if (docResult.status === "rejected") {
892
+ api.logger.warn(`memorylake-openclaw: document search failed: ${String(docResult.reason)}`);
741
893
  }
894
+
895
+ if (contextParts.length === 0) return;
896
+
897
+ return { prependContext: contextParts.join("\n\n") };
742
898
  });
743
899
  }
744
900
 
@@ -789,11 +945,14 @@ const memoryPlugin = {
789
945
  }
790
946
 
791
947
  if (!textContent) continue;
792
- // Strip injected memory context, keep the actual user text
948
+ // Strip injected context, keep the actual user text
793
949
  if (textContent.includes("<relevant-memories>")) {
794
950
  textContent = textContent.replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>\s*/g, "").trim();
795
- if (!textContent) continue;
796
951
  }
952
+ if (textContent.includes("<relevant-documents>")) {
953
+ textContent = textContent.replace(/<relevant-documents>[\s\S]*?<\/relevant-documents>\s*/g, "").trim();
954
+ }
955
+ if (!textContent) continue;
797
956
 
798
957
  formattedMessages.push({
799
958
  role: role as string,
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/memorylake-ai/memorylake-openclaw.git"
10
+ },
7
11
  "keywords": [
8
12
  "openclaw",
9
13
  "plugin",