memorylake-openclaw 0.0.0 → 0.0.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/.github/workflows/release.yml +23 -0
- package/LICENSE +21 -0
- package/LICENSES/Apache-2.0.txt +176 -0
- package/NOTICE +30 -0
- package/README.md +77 -0
- package/docs/openclaw.mdx +98 -0
- package/index.ts +832 -0
- package/openclaw.plugin.json +78 -0
- package/package.json +22 -7
|
@@ -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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Powerdrill Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
package/NOTICE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MemoryLake
|
|
2
|
+
Copyright (c) 2025-2026 MemoryLake Team
|
|
3
|
+
|
|
4
|
+
This product includes software developed by third parties.
|
|
5
|
+
|
|
6
|
+
We would like to express our sincere gratitude to all open-source projects and
|
|
7
|
+
their contributors whose work has been incorporated into MemoryLake. Their
|
|
8
|
+
excellent contributions have been instrumental in accelerating our development
|
|
9
|
+
and building a better product for the community.
|
|
10
|
+
|
|
11
|
+
================================================================================
|
|
12
|
+
|
|
13
|
+
Portions of this software are derived from mem0 (https://github.com/mem0ai/mem0)
|
|
14
|
+
|
|
15
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
16
|
+
you may not use this file except in compliance with the License.
|
|
17
|
+
You may obtain a copy of the License at
|
|
18
|
+
|
|
19
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
20
|
+
|
|
21
|
+
Unless required by applicable law or agreed to in writing, software
|
|
22
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
23
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
24
|
+
See the License for the specific language governing permissions and
|
|
25
|
+
limitations under the License.
|
|
26
|
+
|
|
27
|
+
The following files in this project contain code derived from mem0:
|
|
28
|
+
- memorylake/mem0/**
|
|
29
|
+
|
|
30
|
+
These files have been largely modified from their original versions.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# memorylake-openclaw
|
|
2
|
+
|
|
3
|
+
Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, powered by [MemoryLake](https://app.memorylake.ai).
|
|
4
|
+
|
|
5
|
+
Your agent forgets everything between sessions. This plugin fixes that. It watches conversations, extracts what matters, and brings it back when relevant — automatically.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
<!-- <p align="center">
|
|
10
|
+
<img src="../docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
|
|
11
|
+
</p> -->
|
|
12
|
+
|
|
13
|
+
**Auto-Recall** — Before the agent responds, the plugin searches MemoryLake for memories that match the current message and injects them into context.
|
|
14
|
+
|
|
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
|
+
|
|
17
|
+
Both run silently. No prompting, no configuration, no manual calls.
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
openclaw plugins install memorylake-openclaw
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Get an API key from [app.memorylake.ai](https://app.memorylake.ai), then add to your `openclaw.json`:
|
|
26
|
+
|
|
27
|
+
```json5
|
|
28
|
+
// plugins.entries
|
|
29
|
+
"memorylake-openclaw": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"config": {
|
|
32
|
+
"apiKey": "${MEMORYLAKE_API_KEY}",
|
|
33
|
+
"projectId": "proj-...",
|
|
34
|
+
"userId": "your-user-id"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Agent tools
|
|
40
|
+
|
|
41
|
+
The agent gets five tools it can call during conversations:
|
|
42
|
+
|
|
43
|
+
| Tool | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| `memory_search` | Search memories by natural language |
|
|
46
|
+
| `memory_list` | List all stored memories for a user |
|
|
47
|
+
| `memory_store` | Explicitly save a fact |
|
|
48
|
+
| `memory_get` | Retrieve a memory by ID |
|
|
49
|
+
| `memory_forget` | Delete a memory by ID |
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Search memories
|
|
55
|
+
openclaw memorylake search "what languages does the user know"
|
|
56
|
+
|
|
57
|
+
# Stats
|
|
58
|
+
openclaw memorylake stats
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
| Key | Type | Default | |
|
|
64
|
+
|-----|------|---------|---|
|
|
65
|
+
| `apiKey` | `string` | — | **Required.** MemoryLake API key (supports `${MEMORYLAKE_API_KEY}`) |
|
|
66
|
+
| `projectId` | `string` | — | **Required.** MemoryLake project ID |
|
|
67
|
+
| `host` | `string` | `https://app.memorylake.ai` | MemoryLake server endpoint URL |
|
|
68
|
+
| `userId` | `string` | `"default"` | Scope memories per user |
|
|
69
|
+
| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
|
|
70
|
+
| `autoCapture` | `boolean` | `true` | Store facts after each turn |
|
|
71
|
+
| `topK` | `number` | `5` | Max memories per recall |
|
|
72
|
+
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
|
73
|
+
| `rerank` | `boolean` | `true` | Rerank search results |
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -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 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** — Five tools for explicit memory 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": "${MEMORYLAKE_API_KEY}",
|
|
38
|
+
"projectId": "proj-...",
|
|
39
|
+
"userId": "your-user-id"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Agent Tools
|
|
45
|
+
|
|
46
|
+
The agent gets five tools it can call during conversations:
|
|
47
|
+
|
|
48
|
+
| Tool | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| `memory_search` | Search memories by natural language |
|
|
51
|
+
| `memory_list` | List all stored memories for a user |
|
|
52
|
+
| `memory_store` | Explicitly save a fact |
|
|
53
|
+
| `memory_get` | Retrieve a memory by ID |
|
|
54
|
+
| `memory_forget` | Delete a memory by ID |
|
|
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** — Five agent tools for explicit memory 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
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Memory (MemoryLake) Plugin
|
|
3
|
+
*
|
|
4
|
+
* Long-term memory via MemoryLake platform.
|
|
5
|
+
*
|
|
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
|
|
9
|
+
* - Auto-capture: stores key facts scoped to the current session after each agent turn
|
|
10
|
+
* - CLI: openclaw memorylake search, openclaw memorylake stats
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import got from "got";
|
|
14
|
+
import { Type } from "@sinclair/typebox";
|
|
15
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
type MemoryLakeConfig = {
|
|
22
|
+
host: string;
|
|
23
|
+
apiKey: string;
|
|
24
|
+
projectId: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
autoCapture: boolean;
|
|
27
|
+
autoRecall: boolean;
|
|
28
|
+
searchThreshold: number;
|
|
29
|
+
topK: number;
|
|
30
|
+
rerank: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// V2 API option types
|
|
34
|
+
interface AddOptions {
|
|
35
|
+
user_id: string;
|
|
36
|
+
chat_session_id?: string;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
infer?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SearchOptions {
|
|
42
|
+
user_id: string;
|
|
43
|
+
top_k?: number;
|
|
44
|
+
threshold?: number;
|
|
45
|
+
rerank?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ListOptions {
|
|
49
|
+
user_id?: string;
|
|
50
|
+
page?: number;
|
|
51
|
+
size?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface MemoryItem {
|
|
55
|
+
id: string;
|
|
56
|
+
content: string;
|
|
57
|
+
user_id?: string;
|
|
58
|
+
created_at?: string;
|
|
59
|
+
updated_at?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface AddResultItem {
|
|
63
|
+
event_id: string;
|
|
64
|
+
status: string;
|
|
65
|
+
message: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AddResult {
|
|
69
|
+
results: AddResultItem[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Unified Provider Interface
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
interface MemoryLakeProvider {
|
|
77
|
+
add(
|
|
78
|
+
messages: Array<{ role: string; content: string }>,
|
|
79
|
+
options: AddOptions,
|
|
80
|
+
): Promise<AddResult>;
|
|
81
|
+
search(query: string, options: SearchOptions): Promise<MemoryItem[]>;
|
|
82
|
+
get(memoryId: string): Promise<MemoryItem>;
|
|
83
|
+
getAll(options: ListOptions): Promise<MemoryItem[]>;
|
|
84
|
+
delete(memoryId: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Platform Provider (MemoryLake Cloud)
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
interface ApiResponse<T = unknown> {
|
|
92
|
+
success: boolean;
|
|
93
|
+
message?: string;
|
|
94
|
+
data?: T;
|
|
95
|
+
error_code?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class PlatformProvider implements MemoryLakeProvider {
|
|
99
|
+
private readonly http: ReturnType<typeof got.extend>;
|
|
100
|
+
private readonly basePath: string;
|
|
101
|
+
|
|
102
|
+
constructor(host: string, apiKey: string, projectId: string) {
|
|
103
|
+
this.basePath = `openapi/memorylake/api/v2/projects/${projectId}/memories`;
|
|
104
|
+
this.http = got.extend({
|
|
105
|
+
prefixUrl: host,
|
|
106
|
+
headers: {
|
|
107
|
+
Authorization: `Bearer ${apiKey}`,
|
|
108
|
+
},
|
|
109
|
+
responseType: "json",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async add(
|
|
114
|
+
messages: Array<{ role: string; content: string }>,
|
|
115
|
+
options: AddOptions,
|
|
116
|
+
): Promise<AddResult> {
|
|
117
|
+
const body: Record<string, unknown> = {
|
|
118
|
+
messages,
|
|
119
|
+
user_id: options.user_id,
|
|
120
|
+
infer: options.infer ?? true,
|
|
121
|
+
};
|
|
122
|
+
if (options.chat_session_id) body.chat_session_id = options.chat_session_id;
|
|
123
|
+
if (options.metadata) body.metadata = options.metadata;
|
|
124
|
+
|
|
125
|
+
const resp = await this.http
|
|
126
|
+
.post(this.basePath, { json: body })
|
|
127
|
+
.json<ApiResponse>();
|
|
128
|
+
if (!resp.success) throw new Error(resp.message ?? "add failed");
|
|
129
|
+
return normalizeAddResult(resp.data);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
|
133
|
+
const body: Record<string, unknown> = {
|
|
134
|
+
query,
|
|
135
|
+
user_id: options.user_id,
|
|
136
|
+
};
|
|
137
|
+
if (options.top_k != null) body.top_k = options.top_k;
|
|
138
|
+
if (options.threshold != null) body.threshold = options.threshold;
|
|
139
|
+
if (options.rerank != null) body.rerank = options.rerank;
|
|
140
|
+
|
|
141
|
+
const resp = await this.http
|
|
142
|
+
.post(`${this.basePath}/search`, { json: body })
|
|
143
|
+
.json<ApiResponse>();
|
|
144
|
+
if (!resp.success) throw new Error(resp.message ?? "search failed");
|
|
145
|
+
return normalizeSearchResults(resp.data);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async get(memoryId: string): Promise<MemoryItem> {
|
|
149
|
+
const resp = await this.http
|
|
150
|
+
.get(`${this.basePath}/${memoryId}`)
|
|
151
|
+
.json<ApiResponse>();
|
|
152
|
+
if (!resp.success) throw new Error(resp.message ?? "get failed");
|
|
153
|
+
return normalizeMemoryItem(resp.data);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
|
157
|
+
const searchParams: Record<string, string | number> = {};
|
|
158
|
+
if (options.user_id) searchParams.user_id = options.user_id;
|
|
159
|
+
if (options.page != null) searchParams.page = options.page;
|
|
160
|
+
if (options.size != null) searchParams.size = options.size;
|
|
161
|
+
|
|
162
|
+
const resp = await this.http
|
|
163
|
+
.get(this.basePath, { searchParams })
|
|
164
|
+
.json<ApiResponse>();
|
|
165
|
+
if (!resp.success) throw new Error(resp.message ?? "getAll failed");
|
|
166
|
+
const data = resp.data as any;
|
|
167
|
+
if (data?.items && Array.isArray(data.items))
|
|
168
|
+
return data.items.map(normalizeMemoryItem);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async delete(memoryId: string): Promise<void> {
|
|
173
|
+
const resp = await this.http
|
|
174
|
+
.delete(`${this.basePath}/${memoryId}`)
|
|
175
|
+
.json<ApiResponse>();
|
|
176
|
+
if (!resp.success) throw new Error(resp.message ?? "delete failed");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// Result Normalizers
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
function normalizeMemoryItem(raw: any): MemoryItem {
|
|
185
|
+
return {
|
|
186
|
+
id: raw.id ?? "",
|
|
187
|
+
content: raw.content ?? "",
|
|
188
|
+
user_id: raw.user_id,
|
|
189
|
+
created_at: raw.created_at,
|
|
190
|
+
updated_at: raw.updated_at,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function normalizeSearchResults(raw: any): MemoryItem[] {
|
|
195
|
+
if (raw?.results && Array.isArray(raw.results))
|
|
196
|
+
return raw.results.map(normalizeMemoryItem);
|
|
197
|
+
if (Array.isArray(raw)) return raw.map(normalizeMemoryItem);
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeAddResult(raw: any): AddResult {
|
|
202
|
+
const items = raw?.results ?? (Array.isArray(raw) ? raw : []);
|
|
203
|
+
return {
|
|
204
|
+
results: items.map((r: any) => ({
|
|
205
|
+
event_id: r.event_id ?? "",
|
|
206
|
+
status: r.status ?? "",
|
|
207
|
+
message: r.message ?? "",
|
|
208
|
+
})),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Config Parser
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Config Schema
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
220
|
+
const ALLOWED_KEYS = [
|
|
221
|
+
"host",
|
|
222
|
+
"apiKey",
|
|
223
|
+
"projectId",
|
|
224
|
+
"userId",
|
|
225
|
+
"autoCapture",
|
|
226
|
+
"autoRecall",
|
|
227
|
+
"searchThreshold",
|
|
228
|
+
"topK",
|
|
229
|
+
"rerank",
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
function assertAllowedKeys(
|
|
233
|
+
value: Record<string, unknown>,
|
|
234
|
+
allowed: string[],
|
|
235
|
+
label: string,
|
|
236
|
+
) {
|
|
237
|
+
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
238
|
+
if (unknown.length === 0) return;
|
|
239
|
+
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const memoryLakeConfigSchema = {
|
|
243
|
+
parse(value: unknown): MemoryLakeConfig {
|
|
244
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
245
|
+
throw new Error("memorylake-openclaw config required");
|
|
246
|
+
}
|
|
247
|
+
const cfg = value as Record<string, unknown>;
|
|
248
|
+
assertAllowedKeys(cfg, ALLOWED_KEYS, "memorylake-openclaw config");
|
|
249
|
+
|
|
250
|
+
if (typeof cfg.apiKey !== "string" || !cfg.apiKey) {
|
|
251
|
+
throw new Error("apiKey is required");
|
|
252
|
+
}
|
|
253
|
+
if (typeof cfg.projectId !== "string" || !cfg.projectId) {
|
|
254
|
+
throw new Error("projectId is required");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
host:
|
|
259
|
+
typeof cfg.host === "string" && cfg.host
|
|
260
|
+
? cfg.host
|
|
261
|
+
: "https://app.memorylake.ai",
|
|
262
|
+
apiKey: cfg.apiKey as string,
|
|
263
|
+
projectId: cfg.projectId as string,
|
|
264
|
+
userId:
|
|
265
|
+
typeof cfg.userId === "string" && cfg.userId ? cfg.userId : "default",
|
|
266
|
+
autoCapture: cfg.autoCapture !== false,
|
|
267
|
+
autoRecall: cfg.autoRecall !== false,
|
|
268
|
+
searchThreshold:
|
|
269
|
+
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.3,
|
|
270
|
+
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
|
271
|
+
rerank: cfg.rerank !== false,
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Plugin Definition
|
|
278
|
+
// ============================================================================
|
|
279
|
+
|
|
280
|
+
const memoryPlugin = {
|
|
281
|
+
id: "memorylake-openclaw",
|
|
282
|
+
name: "Memory (MemoryLake)",
|
|
283
|
+
description: "MemoryLake memory backend for OpenClaw",
|
|
284
|
+
kind: "memory" as const,
|
|
285
|
+
configSchema: memoryLakeConfigSchema,
|
|
286
|
+
|
|
287
|
+
register(api: OpenClawPluginApi) {
|
|
288
|
+
const cfg = memoryLakeConfigSchema.parse(api.pluginConfig);
|
|
289
|
+
const provider: MemoryLakeProvider = new PlatformProvider(cfg.host, cfg.apiKey, cfg.projectId);
|
|
290
|
+
|
|
291
|
+
// Track current session ID for tool-level session scoping
|
|
292
|
+
let currentSessionId: string | undefined;
|
|
293
|
+
|
|
294
|
+
api.logger.info(
|
|
295
|
+
`memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Helper: build add options
|
|
299
|
+
function buildAddOptions(userIdOverride?: string, sessionId?: string): AddOptions {
|
|
300
|
+
const opts: AddOptions = {
|
|
301
|
+
user_id: userIdOverride || cfg.userId,
|
|
302
|
+
infer: true,
|
|
303
|
+
metadata: { source: "OPENCLAW" },
|
|
304
|
+
};
|
|
305
|
+
if (sessionId) opts.chat_session_id = sessionId;
|
|
306
|
+
return opts;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Helper: build search options
|
|
310
|
+
function buildSearchOptions(
|
|
311
|
+
userIdOverride?: string,
|
|
312
|
+
limit?: number,
|
|
313
|
+
): SearchOptions {
|
|
314
|
+
return {
|
|
315
|
+
user_id: userIdOverride || cfg.userId,
|
|
316
|
+
top_k: limit ?? cfg.topK,
|
|
317
|
+
threshold: cfg.searchThreshold,
|
|
318
|
+
rerank: cfg.rerank,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ========================================================================
|
|
323
|
+
// Tools
|
|
324
|
+
// ========================================================================
|
|
325
|
+
|
|
326
|
+
api.registerTool(
|
|
327
|
+
{
|
|
328
|
+
name: "memory_search",
|
|
329
|
+
label: "Memory Search",
|
|
330
|
+
description:
|
|
331
|
+
"Search through long-term memories stored in MemoryLake. Use when you need context about user preferences, past decisions, or previously discussed topics.",
|
|
332
|
+
parameters: Type.Object({
|
|
333
|
+
query: Type.String({ description: "Search query" }),
|
|
334
|
+
limit: Type.Optional(
|
|
335
|
+
Type.Number({
|
|
336
|
+
description: `Max results (default: ${cfg.topK})`,
|
|
337
|
+
}),
|
|
338
|
+
),
|
|
339
|
+
userId: Type.Optional(
|
|
340
|
+
Type.String({
|
|
341
|
+
description:
|
|
342
|
+
"User ID to scope search (default: configured userId)",
|
|
343
|
+
}),
|
|
344
|
+
),
|
|
345
|
+
scope: Type.Optional(
|
|
346
|
+
Type.Union([
|
|
347
|
+
Type.Literal("session"),
|
|
348
|
+
Type.Literal("long-term"),
|
|
349
|
+
Type.Literal("all"),
|
|
350
|
+
], {
|
|
351
|
+
description:
|
|
352
|
+
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
|
353
|
+
}),
|
|
354
|
+
),
|
|
355
|
+
}),
|
|
356
|
+
async execute(_toolCallId, params) {
|
|
357
|
+
const { query, limit, userId, scope = "all" } = params as {
|
|
358
|
+
query: string;
|
|
359
|
+
limit?: number;
|
|
360
|
+
userId?: string;
|
|
361
|
+
scope?: "session" | "long-term" | "all";
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const results = await provider.search(
|
|
366
|
+
query,
|
|
367
|
+
buildSearchOptions(userId, limit),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (!results || results.length === 0) {
|
|
371
|
+
return {
|
|
372
|
+
content: [
|
|
373
|
+
{ type: "text", text: "No relevant memories found." },
|
|
374
|
+
],
|
|
375
|
+
details: { count: 0 },
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const text = results
|
|
380
|
+
.map(
|
|
381
|
+
(r, i) =>
|
|
382
|
+
`${i + 1}. ${r.content} (id: ${r.id})`,
|
|
383
|
+
)
|
|
384
|
+
.join("\n");
|
|
385
|
+
|
|
386
|
+
const sanitized = results.map((r) => ({
|
|
387
|
+
id: r.id,
|
|
388
|
+
content: r.content,
|
|
389
|
+
created_at: r.created_at,
|
|
390
|
+
}));
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
content: [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: `Found ${results.length} memories:\n\n${text}`,
|
|
397
|
+
},
|
|
398
|
+
],
|
|
399
|
+
details: { count: results.length, memories: sanitized },
|
|
400
|
+
};
|
|
401
|
+
} catch (err) {
|
|
402
|
+
return {
|
|
403
|
+
content: [
|
|
404
|
+
{
|
|
405
|
+
type: "text",
|
|
406
|
+
text: `Memory search failed: ${String(err)}`,
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
details: { error: String(err) },
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
{ name: "memory_search" },
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
api.registerTool(
|
|
418
|
+
{
|
|
419
|
+
name: "memory_store",
|
|
420
|
+
label: "Memory Store",
|
|
421
|
+
description:
|
|
422
|
+
"Save important information in long-term memory via MemoryLake. Use for preferences, facts, decisions, and anything worth remembering.",
|
|
423
|
+
parameters: Type.Object({
|
|
424
|
+
text: Type.String({ description: "Information to remember" }),
|
|
425
|
+
userId: Type.Optional(
|
|
426
|
+
Type.String({
|
|
427
|
+
description: "User ID to scope this memory",
|
|
428
|
+
}),
|
|
429
|
+
),
|
|
430
|
+
metadata: Type.Optional(
|
|
431
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
432
|
+
description: "Optional metadata to attach to this memory",
|
|
433
|
+
}),
|
|
434
|
+
),
|
|
435
|
+
}),
|
|
436
|
+
async execute(_toolCallId, params) {
|
|
437
|
+
const { text, userId } = params as {
|
|
438
|
+
text: string;
|
|
439
|
+
userId?: string;
|
|
440
|
+
metadata?: Record<string, unknown>;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const result = await provider.add(
|
|
445
|
+
[{ role: "user", content: text }],
|
|
446
|
+
buildAddOptions(userId, currentSessionId),
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
const count = result.results?.length ?? 0;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
content: [
|
|
453
|
+
{
|
|
454
|
+
type: "text",
|
|
455
|
+
text: count > 0
|
|
456
|
+
? `Submitted ${count} memory task(s) for processing. ${result.results.map((r) => `[${r.status}] ${r.message}`).join("; ")}`
|
|
457
|
+
: "No memories extracted.",
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
details: {
|
|
461
|
+
action: "stored",
|
|
462
|
+
results: result.results,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
} catch (err) {
|
|
466
|
+
return {
|
|
467
|
+
content: [
|
|
468
|
+
{
|
|
469
|
+
type: "text",
|
|
470
|
+
text: `Memory store failed: ${String(err)}`,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
details: { error: String(err) },
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
{ name: "memory_store" },
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
api.registerTool(
|
|
482
|
+
{
|
|
483
|
+
name: "memory_get",
|
|
484
|
+
label: "Memory Get",
|
|
485
|
+
description: "Retrieve a specific memory by its ID from MemoryLake.",
|
|
486
|
+
parameters: Type.Object({
|
|
487
|
+
memoryId: Type.String({ description: "The memory ID to retrieve" }),
|
|
488
|
+
}),
|
|
489
|
+
async execute(_toolCallId, params) {
|
|
490
|
+
const { memoryId } = params as { memoryId: string };
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const memory = await provider.get(memoryId);
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
content: [
|
|
497
|
+
{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: `Memory ${memory.id}:\n${memory.content}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}`,
|
|
500
|
+
},
|
|
501
|
+
],
|
|
502
|
+
details: { memory },
|
|
503
|
+
};
|
|
504
|
+
} catch (err) {
|
|
505
|
+
return {
|
|
506
|
+
content: [
|
|
507
|
+
{
|
|
508
|
+
type: "text",
|
|
509
|
+
text: `Memory get failed: ${String(err)}`,
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
details: { error: String(err) },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
{ name: "memory_get" },
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
api.registerTool(
|
|
521
|
+
{
|
|
522
|
+
name: "memory_list",
|
|
523
|
+
label: "Memory List",
|
|
524
|
+
description:
|
|
525
|
+
"List all stored memories for a user. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
|
|
526
|
+
parameters: Type.Object({
|
|
527
|
+
userId: Type.Optional(
|
|
528
|
+
Type.String({
|
|
529
|
+
description:
|
|
530
|
+
"User ID to list memories for (default: configured userId)",
|
|
531
|
+
}),
|
|
532
|
+
),
|
|
533
|
+
scope: Type.Optional(
|
|
534
|
+
Type.Union([
|
|
535
|
+
Type.Literal("session"),
|
|
536
|
+
Type.Literal("long-term"),
|
|
537
|
+
Type.Literal("all"),
|
|
538
|
+
], {
|
|
539
|
+
description:
|
|
540
|
+
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
|
|
541
|
+
}),
|
|
542
|
+
),
|
|
543
|
+
}),
|
|
544
|
+
async execute(_toolCallId, params) {
|
|
545
|
+
const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
const uid = userId || cfg.userId;
|
|
549
|
+
const memories = await provider.getAll({ user_id: uid });
|
|
550
|
+
|
|
551
|
+
if (!memories || memories.length === 0) {
|
|
552
|
+
return {
|
|
553
|
+
content: [
|
|
554
|
+
{ type: "text", text: "No memories stored yet." },
|
|
555
|
+
],
|
|
556
|
+
details: { count: 0 },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const text = memories
|
|
561
|
+
.map(
|
|
562
|
+
(r, i) =>
|
|
563
|
+
`${i + 1}. ${r.content} (id: ${r.id})`,
|
|
564
|
+
)
|
|
565
|
+
.join("\n");
|
|
566
|
+
|
|
567
|
+
const sanitized = memories.map((r) => ({
|
|
568
|
+
id: r.id,
|
|
569
|
+
content: r.content,
|
|
570
|
+
created_at: r.created_at,
|
|
571
|
+
}));
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
content: [
|
|
575
|
+
{
|
|
576
|
+
type: "text",
|
|
577
|
+
text: `${memories.length} memories:\n\n${text}`,
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
details: { count: memories.length, memories: sanitized },
|
|
581
|
+
};
|
|
582
|
+
} catch (err) {
|
|
583
|
+
return {
|
|
584
|
+
content: [
|
|
585
|
+
{
|
|
586
|
+
type: "text",
|
|
587
|
+
text: `Memory list failed: ${String(err)}`,
|
|
588
|
+
},
|
|
589
|
+
],
|
|
590
|
+
details: { error: String(err) },
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
{ name: "memory_list" },
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
api.registerTool(
|
|
599
|
+
{
|
|
600
|
+
name: "memory_forget",
|
|
601
|
+
label: "Memory Forget",
|
|
602
|
+
description:
|
|
603
|
+
"Delete a specific memory by ID from MemoryLake.",
|
|
604
|
+
parameters: Type.Object({
|
|
605
|
+
memoryId: Type.String({ description: "Memory ID to delete" }),
|
|
606
|
+
}),
|
|
607
|
+
async execute(_toolCallId, params) {
|
|
608
|
+
const { memoryId } = params as { memoryId: string };
|
|
609
|
+
|
|
610
|
+
try {
|
|
611
|
+
await provider.delete(memoryId);
|
|
612
|
+
return {
|
|
613
|
+
content: [
|
|
614
|
+
{ type: "text", text: `Memory ${memoryId} forgotten.` },
|
|
615
|
+
],
|
|
616
|
+
details: { action: "deleted", id: memoryId },
|
|
617
|
+
};
|
|
618
|
+
} catch (err) {
|
|
619
|
+
return {
|
|
620
|
+
content: [
|
|
621
|
+
{
|
|
622
|
+
type: "text",
|
|
623
|
+
text: `Memory forget failed: ${String(err)}`,
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
details: { error: String(err) },
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
{ name: "memory_forget" },
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
// ========================================================================
|
|
635
|
+
// CLI Commands
|
|
636
|
+
// ========================================================================
|
|
637
|
+
|
|
638
|
+
api.registerCli(
|
|
639
|
+
({ program }) => {
|
|
640
|
+
const memorylake = program
|
|
641
|
+
.command("memorylake")
|
|
642
|
+
.description("MemoryLake memory plugin commands");
|
|
643
|
+
|
|
644
|
+
memorylake
|
|
645
|
+
.command("search")
|
|
646
|
+
.description("Search memories in MemoryLake")
|
|
647
|
+
.argument("<query>", "Search query")
|
|
648
|
+
.option("--limit <n>", "Max results", String(cfg.topK))
|
|
649
|
+
.action(async (query: string, opts: { limit: string }) => {
|
|
650
|
+
try {
|
|
651
|
+
const limit = parseInt(opts.limit, 10);
|
|
652
|
+
const results = await provider.search(
|
|
653
|
+
query,
|
|
654
|
+
buildSearchOptions(undefined, limit),
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
if (!results.length) {
|
|
658
|
+
console.log("No memories found.");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const output = results.map((r) => ({
|
|
663
|
+
id: r.id,
|
|
664
|
+
content: r.content,
|
|
665
|
+
user_id: r.user_id,
|
|
666
|
+
created_at: r.created_at,
|
|
667
|
+
}));
|
|
668
|
+
console.log(JSON.stringify(output, null, 2));
|
|
669
|
+
} catch (err) {
|
|
670
|
+
console.error(`Search failed: ${String(err)}`);
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
memorylake
|
|
675
|
+
.command("stats")
|
|
676
|
+
.description("Show memory statistics from MemoryLake")
|
|
677
|
+
.action(async () => {
|
|
678
|
+
try {
|
|
679
|
+
const memories = await provider.getAll({
|
|
680
|
+
user_id: cfg.userId,
|
|
681
|
+
});
|
|
682
|
+
console.log(`User: ${cfg.userId}`);
|
|
683
|
+
console.log(
|
|
684
|
+
`Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`,
|
|
685
|
+
);
|
|
686
|
+
console.log(
|
|
687
|
+
`Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`,
|
|
688
|
+
);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`Stats failed: ${String(err)}`);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
},
|
|
694
|
+
{ commands: ["memorylake"] },
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// ========================================================================
|
|
698
|
+
// Lifecycle Hooks
|
|
699
|
+
// ========================================================================
|
|
700
|
+
|
|
701
|
+
// Auto-recall: inject relevant memories before agent starts
|
|
702
|
+
if (cfg.autoRecall) {
|
|
703
|
+
api.on("before_agent_start", async (event, ctx) => {
|
|
704
|
+
if (!event.prompt || event.prompt.length < 5) return;
|
|
705
|
+
|
|
706
|
+
// Track session ID
|
|
707
|
+
const sessionId = (ctx as any)?.sessionKey ?? undefined;
|
|
708
|
+
if (sessionId) currentSessionId = sessionId;
|
|
709
|
+
|
|
710
|
+
try {
|
|
711
|
+
const results = await provider.search(
|
|
712
|
+
event.prompt,
|
|
713
|
+
buildSearchOptions(),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
if (results.length === 0) return;
|
|
717
|
+
|
|
718
|
+
const memoryContext = results
|
|
719
|
+
.map((r) => `- ${r.content}`)
|
|
720
|
+
.join("\n");
|
|
721
|
+
|
|
722
|
+
api.logger.info(
|
|
723
|
+
`memorylake-openclaw: injecting ${results.length} memories into context`,
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
prependContext: `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
|
|
728
|
+
};
|
|
729
|
+
} catch (err) {
|
|
730
|
+
api.logger.warn(`memorylake-openclaw: recall failed: ${String(err)}`);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Auto-capture: store conversation context after agent ends
|
|
736
|
+
if (cfg.autoCapture) {
|
|
737
|
+
api.on("agent_end", async (event, ctx) => {
|
|
738
|
+
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Track session ID
|
|
743
|
+
const sessionId = (ctx as any)?.sessionKey ?? undefined;
|
|
744
|
+
if (sessionId) currentSessionId = sessionId;
|
|
745
|
+
|
|
746
|
+
try {
|
|
747
|
+
// Extract messages, limiting to last 10
|
|
748
|
+
const recentMessages = event.messages.slice(-10);
|
|
749
|
+
const formattedMessages: Array<{
|
|
750
|
+
role: string;
|
|
751
|
+
content: string;
|
|
752
|
+
}> = [];
|
|
753
|
+
|
|
754
|
+
for (const msg of recentMessages) {
|
|
755
|
+
if (!msg || typeof msg !== "object") continue;
|
|
756
|
+
const msgObj = msg as Record<string, unknown>;
|
|
757
|
+
|
|
758
|
+
const role = msgObj.role;
|
|
759
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
760
|
+
|
|
761
|
+
let textContent = "";
|
|
762
|
+
const content = msgObj.content;
|
|
763
|
+
|
|
764
|
+
if (typeof content === "string") {
|
|
765
|
+
textContent = content;
|
|
766
|
+
} else if (Array.isArray(content)) {
|
|
767
|
+
for (const block of content) {
|
|
768
|
+
if (
|
|
769
|
+
block &&
|
|
770
|
+
typeof block === "object" &&
|
|
771
|
+
"text" in block &&
|
|
772
|
+
typeof (block as Record<string, unknown>).text === "string"
|
|
773
|
+
) {
|
|
774
|
+
textContent +=
|
|
775
|
+
(textContent ? "\n" : "") +
|
|
776
|
+
((block as Record<string, unknown>).text as string);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (!textContent) continue;
|
|
782
|
+
// Strip injected memory context, keep the actual user text
|
|
783
|
+
if (textContent.includes("<relevant-memories>")) {
|
|
784
|
+
textContent = textContent.replace(/<relevant-memories>[\s\S]*?<\/relevant-memories>\s*/g, "").trim();
|
|
785
|
+
if (!textContent) continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
formattedMessages.push({
|
|
789
|
+
role: role as string,
|
|
790
|
+
content: textContent,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (formattedMessages.length === 0) return;
|
|
795
|
+
|
|
796
|
+
const addOpts = buildAddOptions(undefined, currentSessionId);
|
|
797
|
+
const result = await provider.add(
|
|
798
|
+
formattedMessages,
|
|
799
|
+
addOpts,
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
const capturedCount = result.results?.length ?? 0;
|
|
803
|
+
if (capturedCount > 0) {
|
|
804
|
+
api.logger.info(
|
|
805
|
+
`memorylake-openclaw: auto-captured ${capturedCount} memories`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
} catch (err) {
|
|
809
|
+
api.logger.warn(`memorylake-openclaw: capture failed: ${String(err)}`);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// ========================================================================
|
|
815
|
+
// Service
|
|
816
|
+
// ========================================================================
|
|
817
|
+
|
|
818
|
+
api.registerService({
|
|
819
|
+
id: "memorylake-openclaw",
|
|
820
|
+
start: () => {
|
|
821
|
+
api.logger.info(
|
|
822
|
+
`memorylake-openclaw: initialized (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture})`,
|
|
823
|
+
);
|
|
824
|
+
},
|
|
825
|
+
stop: () => {
|
|
826
|
+
api.logger.info("memorylake-openclaw: stopped");
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
export default memoryPlugin;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "memorylake-openclaw",
|
|
3
|
+
"kind": "memory",
|
|
4
|
+
"uiHints": {
|
|
5
|
+
"apiKey": {
|
|
6
|
+
"label": "MemoryLake API Key",
|
|
7
|
+
"sensitive": true,
|
|
8
|
+
"placeholder": "sk-...",
|
|
9
|
+
"help": "API key from app.memorylake.ai (or use ${MEMORYLAKE_API_KEY})."
|
|
10
|
+
},
|
|
11
|
+
"projectId": {
|
|
12
|
+
"label": "Project ID",
|
|
13
|
+
"placeholder": "proj-...",
|
|
14
|
+
"help": "MemoryLake project ID. Required for all memory operations."
|
|
15
|
+
},
|
|
16
|
+
"userId": {
|
|
17
|
+
"label": "Default User ID",
|
|
18
|
+
"placeholder": "default",
|
|
19
|
+
"help": "User ID for scoping memories"
|
|
20
|
+
},
|
|
21
|
+
"autoCapture": {
|
|
22
|
+
"label": "Auto-Capture",
|
|
23
|
+
"help": "Automatically store conversation context after each agent turn"
|
|
24
|
+
},
|
|
25
|
+
"autoRecall": {
|
|
26
|
+
"label": "Auto-Recall",
|
|
27
|
+
"help": "Automatically inject relevant memories before each agent turn"
|
|
28
|
+
},
|
|
29
|
+
"searchThreshold": {
|
|
30
|
+
"label": "Search Threshold",
|
|
31
|
+
"placeholder": "0.3",
|
|
32
|
+
"help": "Minimum similarity score for search results (0-1). Default: 0.3"
|
|
33
|
+
},
|
|
34
|
+
"topK": {
|
|
35
|
+
"label": "Top K Results",
|
|
36
|
+
"placeholder": "5",
|
|
37
|
+
"help": "Maximum number of memories to retrieve"
|
|
38
|
+
},
|
|
39
|
+
"rerank": {
|
|
40
|
+
"label": "Rerank",
|
|
41
|
+
"help": "Rerank search results"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"configSchema": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"properties": {
|
|
48
|
+
"host": {
|
|
49
|
+
"type": "string"
|
|
50
|
+
},
|
|
51
|
+
"apiKey": {
|
|
52
|
+
"type": "string"
|
|
53
|
+
},
|
|
54
|
+
"projectId": {
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
"userId": {
|
|
58
|
+
"type": "string"
|
|
59
|
+
},
|
|
60
|
+
"autoCapture": {
|
|
61
|
+
"type": "boolean"
|
|
62
|
+
},
|
|
63
|
+
"autoRecall": {
|
|
64
|
+
"type": "boolean"
|
|
65
|
+
},
|
|
66
|
+
"searchThreshold": {
|
|
67
|
+
"type": "number"
|
|
68
|
+
},
|
|
69
|
+
"topK": {
|
|
70
|
+
"type": "number"
|
|
71
|
+
},
|
|
72
|
+
"rerank": {
|
|
73
|
+
"type": "boolean"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"required": []
|
|
77
|
+
}
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorylake-openclaw",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MemoryLake memory backend for OpenClaw",
|
|
5
6
|
"license": "MIT",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/memorylake-ai/memorylake-openclaw.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"openclaw",
|
|
13
|
+
"plugin",
|
|
14
|
+
"memory",
|
|
15
|
+
"memorylake",
|
|
16
|
+
"long-term-memory"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@sinclair/typebox": "0.34.47",
|
|
20
|
+
"got": "^14.0.0"
|
|
21
|
+
},
|
|
22
|
+
"openclaw": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.ts"
|
|
25
|
+
]
|
|
11
26
|
}
|
|
12
27
|
}
|