memorylake-openclaw 0.0.7 → 0.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",
@@ -0,0 +1,200 @@
1
+ ---
2
+ name: memorylake-api
3
+ description: Use when the user asks about MemoryLake features, capabilities, or wants to perform a MemoryLake action but no specific tool or skill matches. This is the catch-all for any MemoryLake-related request -- discovers available APIs from the remote OpenAPI spec and calls them directly.
4
+ ---
5
+
6
+ # MemoryLake API
7
+
8
+ ## Overview
9
+
10
+ Directly call MemoryLake's REST APIs by discovering endpoints from the live OpenAPI spec. This skill covers any MemoryLake capability not already handled by a dedicated tool or skill — project management, document management, file uploads, memory trace, statistics, and more.
11
+
12
+ ## When to Use
13
+
14
+ - User asks about MemoryLake capabilities or features, and no existing tool or skill covers the request
15
+ - User wants to manage MemoryLake projects (create, update, delete, list, view stats)
16
+ - User wants to manage documents (upload files, add to project, list, delete)
17
+ - User wants to view the change history (trace) of a memory
18
+ - User wants to call any MemoryLake API endpoint directly
19
+
20
+ ## Step 1 — Read MemoryLake Config
21
+
22
+ Read `~/.openclaw/openclaw.json` and extract the plugin config:
23
+
24
+ ```bash
25
+ cat ~/.openclaw/openclaw.json | jq '.plugins.entries["memorylake-openclaw"].config'
26
+ ```
27
+
28
+ Extract these values:
29
+
30
+ | Variable | Description | Default |
31
+ |----------|-------------|---------|
32
+ | `host` | API host | `https://app.memorylake.ai` |
33
+ | `apiKey` | API key for authentication | (required) |
34
+ | `projectId` | MemoryLake project ID | (required) |
35
+
36
+ If `apiKey` or `projectId` is missing, stop and inform the user.
37
+
38
+ Auth header for all requests:
39
+
40
+ ```
41
+ Authorization: Bearer {apiKey}
42
+ ```
43
+
44
+ ## Step 2 — Identify the Right API Endpoint
45
+
46
+ ### Option A: Use the Quick Reference Table
47
+
48
+ If the user's intent clearly maps to one of the endpoints below, skip to Step 3.
49
+
50
+ #### Projects
51
+
52
+ | Method | Path | Description |
53
+ |--------|------|-------------|
54
+ | `GET` | `/api/v1/projects` | List projects (paginated, filterable by name) |
55
+ | `POST` | `/api/v1/projects` | Create a new project |
56
+ | `GET` | `/api/v1/projects/{id}` | Get project details (includes stats) |
57
+ | `PUT` | `/api/v1/projects/{id}` | Update project name/description |
58
+ | `DELETE` | `/api/v1/projects/{id}` | Delete a project |
59
+
60
+ #### Memories (V2)
61
+
62
+ | Method | Path | Description |
63
+ |--------|------|-------------|
64
+ | `POST` | `/api/v2/projects/{id}/memories` | Add memory from conversation |
65
+ | `GET` | `/api/v2/projects/{id}/memories` | List memories (paginated, filter by `user_id`/`keyword`) |
66
+ | `POST` | `/api/v2/projects/{id}/memories/search` | Search memories by natural language |
67
+ | `GET` | `/api/v2/projects/{id}/memories/{memoryId}` | Get a single memory |
68
+ | `DELETE` | `/api/v2/projects/{id}/memories/{memoryId}` | Delete a memory |
69
+ | `GET` | `/api/v2/projects/{id}/memories/{memoryId}/trace` | Get memory change history |
70
+
71
+ #### Documents
72
+
73
+ | Method | Path | Description |
74
+ |--------|------|-------------|
75
+ | `GET` | `/api/v1/projects/{id}/documents` | List documents in project (paginated) |
76
+ | `GET` | `/api/v1/projects/{id}/documents/{documentId}` | Get document details |
77
+ | `DELETE` | `/api/v1/projects/{id}/documents` | Batch delete documents |
78
+ | `POST` | `/api/v1/projects/{id}/documents/search` | Semantic search over documents |
79
+
80
+ ### Option B: Fetch the Live OpenAPI Spec
81
+
82
+ If the user's request is ambiguous or might involve a new/undocumented endpoint, fetch the spec:
83
+
84
+ ```bash
85
+ curl -s "{host}/openapi/memorylake/api-docs/open-api" | jq '.paths | keys'
86
+ ```
87
+
88
+ To inspect a specific endpoint's schema:
89
+
90
+ ```bash
91
+ curl -s "{host}/openapi/memorylake/api-docs/open-api" | jq '.paths["/api/v1/projects/{id}"]'
92
+ ```
93
+
94
+ To inspect request/response schemas:
95
+
96
+ ```bash
97
+ curl -s "{host}/openapi/memorylake/api-docs/open-api" | jq '.components.schemas["ProjectCreateRequest"]'
98
+ ```
99
+
100
+ ## Step 3 — Construct and Execute the API Call
101
+
102
+ Build a `curl` command using:
103
+
104
+ - **Base URL**: `{host}/openapi/memorylake` (the server base path from the OpenAPI spec)
105
+ - **Full URL**: `{host}/openapi/memorylake{path}` (e.g., `{host}/openapi/memorylake/api/v1/projects`)
106
+ - **Path params**: replace `{id}` with `{projectId}` from config (for project-scoped endpoints)
107
+ - **Auth header**: `Authorization: Bearer {apiKey}`
108
+ - **Content-Type**: `application/json` (for POST/PUT requests with a body)
109
+
110
+ ### Example: List Projects
111
+
112
+ ```bash
113
+ curl -s -X GET "{host}/openapi/memorylake/api/v1/projects?page=1&size=20" \
114
+ -H "Authorization: Bearer {apiKey}" | jq
115
+ ```
116
+
117
+ ### Example: Create a Project
118
+
119
+ ```bash
120
+ curl -s -X POST "{host}/openapi/memorylake/api/v1/projects" \
121
+ -H "Authorization: Bearer {apiKey}" \
122
+ -H "Content-Type: application/json" \
123
+ -d '{
124
+ "name": "My New Project",
125
+ "description": "Project description"
126
+ }' | jq
127
+ ```
128
+
129
+ ### Example: Get Project Details (with Stats)
130
+
131
+ ```bash
132
+ curl -s -X GET "{host}/openapi/memorylake/api/v1/projects/{projectId}" \
133
+ -H "Authorization: Bearer {apiKey}" | jq
134
+ ```
135
+
136
+ ### Example: Get Memory Trace
137
+
138
+ ```bash
139
+ curl -s -X GET "{host}/openapi/memorylake/api/v2/projects/{projectId}/memories/{memoryId}/trace" \
140
+ -H "Authorization: Bearer {apiKey}" | jq
141
+ ```
142
+
143
+ ### Example: Search Documents
144
+
145
+ ```bash
146
+ curl -s -X POST "{host}/openapi/memorylake/api/v1/projects/{projectId}/documents/search" \
147
+ -H "Authorization: Bearer {apiKey}" \
148
+ -H "Content-Type: application/json" \
149
+ -d '{
150
+ "query": "quarterly sales figures",
151
+ "top_n": 10
152
+ }' | jq
153
+ ```
154
+
155
+ ## Step 4 — Present Results
156
+
157
+ Parse the JSON response and present it to the user in a readable format:
158
+
159
+ - Check `success` field — if `false`, report `message` and `error_code`
160
+ - For list/search responses, format the `data.items` or `data.results` array as a table or structured list
161
+ - For single-item responses, display key fields clearly
162
+ - For paginated responses, report `page`, `total`, `total_pages` so the user knows if there are more results
163
+
164
+ ## Error Handling
165
+
166
+ All responses follow the same wrapper format:
167
+
168
+ ```json
169
+ {
170
+ "success": true|false,
171
+ "message": "Human-readable message",
172
+ "data": { ... },
173
+ "error_code": "VALIDATION_ERROR"
174
+ }
175
+ ```
176
+
177
+ | HTTP Status | Meaning | Action |
178
+ |-------------|---------|--------|
179
+ | 200 | Success | Parse `data` field |
180
+ | 400 | Invalid request | Check `message` for validation details |
181
+ | 404 | Not found | Verify project ID / memory ID / document ID |
182
+ | 401/403 | Auth failure | Verify `apiKey` is correct and not expired |
183
+
184
+ ## Common Mistakes
185
+
186
+ - **Wrong base URL**: The full URL must include `/openapi/memorylake` before the API path. E.g., `/openapi/memorylake/api/v1/projects`, NOT just `/api/v1/projects`
187
+ - **Missing auth header**: Every request requires `Authorization: Bearer {apiKey}`
188
+ - **Hardcoded project ID**: Always read `projectId` from `~/.openclaw/openclaw.json` config, not from user input (unless the user explicitly wants a different project)
189
+ - **Pagination**: List endpoints default to `page=1, size=20`. Pass `page` and `size` query params if the user needs more results
190
+
191
+ ## Quick Reference
192
+
193
+ | Item | Value |
194
+ |------|-------|
195
+ | OpenClaw config | `~/.openclaw/openclaw.json` |
196
+ | Plugin config key | `plugins.entries["memorylake-openclaw"].config` |
197
+ | Server base path | `/openapi/memorylake` |
198
+ | OpenAPI spec URL | `{host}/openapi/memorylake/api-docs/open-api` |
199
+ | Auth header | `Authorization: Bearer {apiKey}` |
200
+ | Default host | `https://app.memorylake.ai` |
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: memorylake-upload
3
+ description: Use when the user wants to upload files, documents, PDFs, or other data files to MemoryLake and associate them with a project.
4
+ ---
5
+
6
+ # MemoryLake File Upload
7
+
8
+ ## Overview
9
+
10
+ Upload local files to MemoryLake using the multipart upload API, then associate them with a project.
11
+
12
+ ## When to Use
13
+
14
+ - User wants to upload a file (PDF, DOCX, image, etc.) to MemoryLake
15
+ - User wants to add a local document to a MemoryLake project
16
+
17
+ ## Step 1 -- Read MemoryLake Config
18
+
19
+ Read `~/.openclaw/openclaw.json` and extract the plugin config:
20
+
21
+ ```bash
22
+ cat ~/.openclaw/openclaw.json | jq '.plugins.entries["memorylake-openclaw"].config'
23
+ ```
24
+
25
+ Extract these values:
26
+
27
+ | Variable | Description | Default |
28
+ |----------|-------------|---------|
29
+ | `host` | API host | `https://app.memorylake.ai` |
30
+ | `apiKey` | API key for authentication | (required) |
31
+ | `projectId` | MemoryLake project ID | (required) |
32
+
33
+ If `apiKey` or `projectId` is missing, stop and inform the user.
34
+
35
+ ## Step 2 -- Run the Upload Script
36
+
37
+ The upload script is at `scripts/upload.mjs` relative to **this skill's SKILL.md**.
38
+
39
+ ```bash
40
+ node {path-to-this-skill}/scripts/upload.mjs \
41
+ --host {host} \
42
+ --api-key {apiKey} \
43
+ --project-id {projectId} \
44
+ --file-name {fileName} \
45
+ /path/to/file
46
+ ```
47
+
48
+ `--file-name` is the original file name as provided by the user (e.g., `report-Q1.pdf`). This is required because the local file path may be a temp path or renamed file that doesn't reflect the real name.
49
+
50
+ ## Step 3 -- Handle Output
51
+
52
+ The script prints progress for each step (create upload, upload parts, complete, add to project).
53
+
54
+ - **Success**: Report the document ID and file name to the user
55
+ - **Failure**: The script prints the specific error (file not found, auth failed, API error). Read the error message and relay it to the user — don't guess the cause
56
+
57
+ ## Common Mistakes
58
+
59
+ - **Skipping Step 1**: Directly hardcoding host/apiKey/projectId instead of reading from `~/.openclaw/openclaw.json`
60
+ - **Relative file paths**: Always resolve the user's file path to an absolute path before passing to the script
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MemoryLake File Upload Script
5
+ *
6
+ * Uploads a local file to MemoryLake using multipart upload,
7
+ * then associates it with a project.
8
+ *
9
+ * Usage:
10
+ * node upload.mjs --host <url> --api-key <key> --project-id <id> <file_path>
11
+ *
12
+ * Parameters:
13
+ * host - Base URL (e.g., http://10.71.10.71:3002)
14
+ * apiKey - API key for authentication
15
+ * projectId - Project ID to associate the document with (required)
16
+ * filePath - Path to the file to upload
17
+ */
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import https from 'https';
22
+ import http from 'http';
23
+
24
+ // API base path
25
+ const API_BASE = '/openapi/memorylake';
26
+
27
+ /**
28
+ * Make an HTTP request
29
+ */
30
+ function request(method, urlStr, body = null, headers = {}) {
31
+ return new Promise((resolve, reject) => {
32
+ const url = new URL(urlStr);
33
+ const isHttps = url.protocol === 'https:';
34
+ const lib = isHttps ? https : http;
35
+
36
+ const options = {
37
+ hostname: url.hostname,
38
+ port: url.port || (isHttps ? 443 : 80),
39
+ path: url.pathname + url.search,
40
+ method,
41
+ headers: {
42
+ ...headers,
43
+ },
44
+ };
45
+
46
+ if (body && typeof body === 'object' && !(body instanceof Buffer)) {
47
+ options.headers['Content-Type'] = 'application/json';
48
+ }
49
+
50
+ const req = lib.request(options, (res) => {
51
+ const chunks = [];
52
+ res.on('data', (chunk) => chunks.push(chunk));
53
+ res.on('end', () => {
54
+ const buffer = Buffer.concat(chunks);
55
+ const text = buffer.toString('utf8');
56
+ resolve({
57
+ status: res.statusCode,
58
+ headers: res.headers,
59
+ body: text,
60
+ });
61
+ });
62
+ });
63
+
64
+ req.on('error', reject);
65
+
66
+ if (body) {
67
+ if (body instanceof Buffer) {
68
+ req.write(body);
69
+ } else if (typeof body === 'object') {
70
+ req.write(JSON.stringify(body));
71
+ } else {
72
+ req.write(body);
73
+ }
74
+ }
75
+
76
+ req.end();
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Create multipart upload session
82
+ */
83
+ async function createMultipartUpload(host, apiKey, fileSize) {
84
+ const url = `${host}${API_BASE}/api/v1/upload/create-multipart`;
85
+ const response = await request('POST', url, { file_size: fileSize }, {
86
+ 'Authorization': `Bearer ${apiKey}`,
87
+ 'Content-Type': 'application/json',
88
+ });
89
+
90
+ const data = JSON.parse(response.body);
91
+ if (!data.success) {
92
+ throw new Error(`Create multipart failed: ${data.message || JSON.stringify(data)}`);
93
+ }
94
+ return data.data;
95
+ }
96
+
97
+ /**
98
+ * Upload a single part to pre-signed URL
99
+ */
100
+ async function uploadPart(uploadUrl, buffer, partNumber, totalParts) {
101
+ process.stdout.write(` Uploading part ${partNumber + 1}/${totalParts}...`);
102
+
103
+ const response = await request('PUT', uploadUrl, buffer, {
104
+ 'Content-Length': buffer.length.toString(),
105
+ });
106
+
107
+ if (response.status < 200 || response.status >= 300) {
108
+ throw new Error(`Part upload failed with status ${response.status}: ${response.body}`);
109
+ }
110
+
111
+ // Get ETag from response headers (may be quoted)
112
+ let etag = response.headers['etag'] || response.headers['ETag'];
113
+ if (etag) {
114
+ etag = etag.replace(/"/g, '');
115
+ }
116
+
117
+ console.log(` done (ETag: ${etag || 'none'})`);
118
+ return etag;
119
+ }
120
+
121
+ /**
122
+ * Complete multipart upload
123
+ */
124
+ async function completeMultipartUpload(host, apiKey, uploadId, objectKey, partEtags) {
125
+ const url = `${host}${API_BASE}/api/v1/upload/complete-multipart`;
126
+ const response = await request('POST', url, {
127
+ upload_id: uploadId,
128
+ object_key: objectKey,
129
+ part_etags: partEtags,
130
+ }, {
131
+ 'Authorization': `Bearer ${apiKey}`,
132
+ 'Content-Type': 'application/json',
133
+ });
134
+
135
+ const data = JSON.parse(response.body);
136
+ if (!data.success) {
137
+ throw new Error(`Complete multipart failed: ${data.message || JSON.stringify(data)}`);
138
+ }
139
+ return data;
140
+ }
141
+
142
+ /**
143
+ * Add document to project
144
+ */
145
+ async function quickAddDocument(host, apiKey, projectId, objectKey, fileName) {
146
+ const url = `${host}${API_BASE}/api/v1/projects/${projectId}/documents/quick-add`;
147
+ const response = await request('POST', url, {
148
+ object_key: objectKey,
149
+ file_name: fileName,
150
+ }, {
151
+ 'Authorization': `Bearer ${apiKey}`,
152
+ 'Content-Type': 'application/json',
153
+ });
154
+
155
+ const data = JSON.parse(response.body);
156
+ if (!data.success) {
157
+ throw new Error(`Quick add document failed: ${data.message || JSON.stringify(data)}`);
158
+ }
159
+ return data.data;
160
+ }
161
+
162
+ /**
163
+ * Format file size for display
164
+ */
165
+ function formatSize(bytes) {
166
+ if (bytes < 1024) return `${bytes} B`;
167
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
168
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
169
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
170
+ }
171
+
172
+ /**
173
+ * Main upload function
174
+ */
175
+ export async function upload({ host, apiKey, projectId, filePath, fileName }) {
176
+ if (!host) throw new Error('host is required');
177
+ if (!apiKey) throw new Error('apiKey is required');
178
+ if (!projectId) throw new Error('projectId is required');
179
+ if (!filePath) throw new Error('filePath is required');
180
+
181
+ if (!fs.existsSync(filePath)) {
182
+ throw new Error(`File not found: ${filePath}`);
183
+ }
184
+
185
+ const stats = fs.statSync(filePath);
186
+ const fileSize = stats.size;
187
+ if (!fileName) fileName = path.basename(filePath);
188
+
189
+ console.log(`\nUploading: ${fileName} (${formatSize(fileSize)})`);
190
+
191
+ // Step 1: Create multipart upload
192
+ console.log('Creating multipart upload...');
193
+ const uploadInfo = await createMultipartUpload(host, apiKey, fileSize);
194
+ const { upload_id, object_key, part_items } = uploadInfo;
195
+ console.log(` Upload ID: ${upload_id}`);
196
+ console.log(` Object Key: ${object_key}`);
197
+ console.log(` Parts: ${part_items.length}`);
198
+
199
+ // Step 2: Upload each part (stream each chunk to avoid loading entire file into memory)
200
+ console.log('\nUploading parts:');
201
+ const fd = fs.openSync(filePath, 'r');
202
+ const partEtags = [];
203
+
204
+ let offset = 0;
205
+ for (const part of part_items) {
206
+ const partBuffer = Buffer.alloc(part.size);
207
+ fs.readSync(fd, partBuffer, 0, part.size, offset);
208
+ const etag = await uploadPart(part.upload_url, partBuffer, part.number, part_items.length);
209
+ partEtags.push({
210
+ number: part.number,
211
+ etag: etag,
212
+ });
213
+ offset += part.size;
214
+ }
215
+ fs.closeSync(fd);
216
+
217
+ // Step 3: Complete multipart upload
218
+ console.log('\nCompleting multipart upload...');
219
+ await completeMultipartUpload(host, apiKey, upload_id, object_key, partEtags);
220
+ console.log(' Upload completed');
221
+
222
+ // Step 4: Add to project
223
+ console.log(`\nAdding to project: ${projectId}`);
224
+ const doc = await quickAddDocument(host, apiKey, projectId, object_key, fileName);
225
+ console.log(' Document added to project');
226
+ console.log(` Document ID: ${doc.document_id}`);
227
+ console.log(` File name: ${doc.file_name}`);
228
+ return doc;
229
+ }
230
+
231
+ // CLI entry point
232
+ async function main() {
233
+ const args = process.argv.slice(2);
234
+
235
+ if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
236
+ console.log(`
237
+ MemoryLake File Upload
238
+
239
+ Usage:
240
+ node upload.mjs --host <url> --api-key <key> --project-id <id> <file_path>
241
+
242
+ Arguments:
243
+ --host Base URL (e.g., http://10.71.10.71:3002)
244
+ --api-key API key for authentication
245
+ --project-id Project ID to associate the document with (required)
246
+ --file-name Custom file name (default: basename of file_path)
247
+ file_path Path to the file to upload
248
+
249
+ Examples:
250
+ node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 document.pdf
251
+ `);
252
+ process.exit(0);
253
+ }
254
+
255
+ let host, apiKey, projectId, filePath, fileName;
256
+ for (let i = 0; i < args.length; i++) {
257
+ if (args[i] === '--host') host = args[++i];
258
+ else if (args[i] === '--api-key') apiKey = args[++i];
259
+ else if (args[i] === '--project-id') projectId = args[++i];
260
+ else if (args[i] === '--file-name') fileName = args[++i];
261
+ else if (!args[i].startsWith('-')) filePath = args[i];
262
+ }
263
+
264
+ try {
265
+ await upload({ host, apiKey, projectId, filePath, fileName });
266
+ console.log('\nDone!\n');
267
+ } catch (err) {
268
+ console.error(`\nError: ${err.message}\n`);
269
+ process.exit(1);
270
+ }
271
+ }
272
+
273
+ main();