memorylake-openclaw 1.0.2-beta.4 → 1.0.2-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -5,9 +5,9 @@
5
5
  *
6
6
  * Features:
7
7
  * - 9 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search, document_download, advanced_web_search, open_data_search
8
- * - Auto-recall: injects memory-search instructions (+ open-data categories) into the system prompt before each agent turn
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
- * - CLI: openclaw memorylake search, openclaw memorylake stats
10
+ * - CLI: openclaw memorylake search, openclaw memorylake stats, openclaw memorylake upload
11
11
  */
12
12
 
13
13
  import fs from "node:fs";
@@ -780,6 +780,15 @@ const memoryLakeConfigSchema = {
780
780
  // Plugin Definition
781
781
  // ============================================================================
782
782
 
783
+ /** Shared type for the upload / uploadAuto function signature */
784
+ type UploadFn = (opts: {
785
+ host: string;
786
+ apiKey: string;
787
+ projectId: string;
788
+ filePath: string;
789
+ fileName: string;
790
+ }) => Promise<unknown>;
791
+
783
792
  const memoryPlugin = {
784
793
  id: "memorylake-openclaw",
785
794
  name: "Memory (MemoryLake)",
@@ -994,23 +1003,6 @@ const memoryPlugin = {
994
1003
  content: r.content,
995
1004
  created_at: r.created_at,
996
1005
  }));
997
-
998
- // Check for unresolved conflicts among returned memories
999
- const conflictMemoryIds = results
1000
- .filter((r) => r.has_unresolved_conflict)
1001
- .map((r) => r.id);
1002
- if (conflictMemoryIds.length > 0) {
1003
- try {
1004
- const effectiveUserId = userId ?? effectiveCfg.userId;
1005
- const conflicts = await effectiveProvider.listConflicts(conflictMemoryIds, effectiveUserId);
1006
- if (conflicts.length > 0) {
1007
- const conflictText = buildConflictContext(conflicts);
1008
- sections.push(`## Memory Conflicts\nThe following memories have unresolved conflicts. Review and help the user resolve them if relevant:\n\n${conflictText}`);
1009
- }
1010
- } catch (err) {
1011
- sections.push(`## Memory Conflicts\nFailed to fetch conflicts: ${String(err)}`);
1012
- }
1013
- }
1014
1006
  } else if (memoryResult.status === "rejected") {
1015
1007
  sections.push(`## Memories\nMemory search failed: ${String(memoryResult.reason)}`);
1016
1008
  }
@@ -1028,7 +1020,7 @@ const memoryPlugin = {
1028
1020
  content: [
1029
1021
  { type: "text", text: "No relevant memories or documents found." },
1030
1022
  ],
1031
- details: { memoryCount: 0, documentCount: 0, memories: [] },
1023
+ details: { count: 0 },
1032
1024
  };
1033
1025
  }
1034
1026
 
@@ -1698,6 +1690,69 @@ const memoryPlugin = {
1698
1690
  }
1699
1691
  });
1700
1692
 
1693
+ memorylake
1694
+ .command("upload")
1695
+ .description("Upload files or directories to MemoryLake")
1696
+ .argument("<path>", "File or directory path to upload")
1697
+ .option("--agent <id>", "Agent ID (resolves workspace and per-agent projectId)")
1698
+ .option("--project-id <id>", "Override project ID (takes precedence over --agent)")
1699
+ .action(async (targetPath: string, opts: { agent?: string; projectId?: string }) => {
1700
+ // Resolve effective config: --project-id > agent workspace config > global config
1701
+ let effectiveCfg = cfg;
1702
+ if (opts.agent) {
1703
+ try {
1704
+ const openclawPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
1705
+ const openclaw = JSON.parse(fs.readFileSync(openclawPath, "utf-8"));
1706
+ const agents = openclaw?.agents;
1707
+ const agentEntry = agents?.list?.find((a: any) => a.id === opts.agent);
1708
+ const workspace = agentEntry?.workspace || agents?.defaults?.workspace;
1709
+ if (workspace) {
1710
+ effectiveCfg = resolveConfig({ workspaceDir: workspace });
1711
+ } else {
1712
+ console.warn(`Warning: no workspace found for agent "${opts.agent}", using global config.`);
1713
+ }
1714
+ } catch (err) {
1715
+ console.warn(`Warning: failed to resolve agent config: ${String(err)}, using global config.`);
1716
+ }
1717
+ }
1718
+ const effectiveProjectId = opts.projectId || effectiveCfg.projectId;
1719
+ if (!effectiveProjectId) {
1720
+ console.error("No project ID configured. Use --project-id or set up agent/workspace config.");
1721
+ return;
1722
+ }
1723
+ if (!effectiveCfg.host || !effectiveCfg.apiKey) {
1724
+ console.error("Missing host or apiKey in config. Check your MemoryLake configuration.");
1725
+ return;
1726
+ }
1727
+
1728
+ // Lazy import upload.mjs (use uploadAuto to support archives)
1729
+ let uploadFn: UploadFn;
1730
+ try {
1731
+ const uploadModule = await import(
1732
+ /* webpackIgnore: true */
1733
+ new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
1734
+ );
1735
+ uploadFn = uploadModule.uploadAuto;
1736
+ } catch (err) {
1737
+ console.error(`Failed to load upload module: ${String(err)}`);
1738
+ return;
1739
+ }
1740
+
1741
+ const absPath = path.resolve(targetPath);
1742
+
1743
+ try {
1744
+ await uploadFn({
1745
+ host: effectiveCfg.host,
1746
+ apiKey: effectiveCfg.apiKey,
1747
+ projectId: effectiveProjectId,
1748
+ filePath: absPath,
1749
+ fileName: path.basename(absPath),
1750
+ });
1751
+ } catch (err) {
1752
+ console.error(`Upload failed: ${String(err)}`);
1753
+ }
1754
+ });
1755
+
1701
1756
  memorylake
1702
1757
  .command("stats")
1703
1758
  .description("Show memory statistics from MemoryLake")
@@ -1774,7 +1829,7 @@ const memoryPlugin = {
1774
1829
  // Auto-upload: upload inbound files to MemoryLake before prompt build
1775
1830
  if (cfg.autoUpload) {
1776
1831
  // Lazy-load upload function from upload.mjs
1777
- let uploadFn: ((opts: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>) | undefined;
1832
+ let uploadAutoFn: UploadFn | undefined;
1778
1833
 
1779
1834
  api.on("before_prompt_build", (event, ctx) => {
1780
1835
  if ((ctx as any)?.trigger !== "user") {
@@ -1799,17 +1854,17 @@ const memoryPlugin = {
1799
1854
  // Fire-and-forget: upload asynchronously without blocking
1800
1855
  (async () => {
1801
1856
  // Lazy import upload.mjs
1802
- if (!uploadFn) {
1857
+ if (!uploadAutoFn) {
1803
1858
  const uploadModule = await import(
1804
1859
  /* webpackIgnore: true */
1805
1860
  new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
1806
1861
  );
1807
- uploadFn = uploadModule.upload;
1862
+ uploadAutoFn = uploadModule.uploadAuto;
1808
1863
  }
1809
1864
 
1810
1865
  for (const { filePath, stat } of filesToUpload) {
1811
1866
  try {
1812
- await uploadFn!({
1867
+ await uploadAutoFn!({
1813
1868
  host: effectiveCfg.host,
1814
1869
  apiKey: effectiveCfg.apiKey,
1815
1870
  projectId: effectiveCfg.projectId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "1.0.2-beta.4",
3
+ "version": "1.0.2-beta.5",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",
@@ -17,7 +17,10 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@sinclair/typebox": "0.34.47",
20
- "got": "^14.0.0"
20
+ "7zip-min": "^3.0.1",
21
+ "adm-zip": "^0.5.17",
22
+ "got": "^14.0.0",
23
+ "tar": "^7.5.13"
21
24
  },
22
25
  "openclaw": {
23
26
  "extensions": [
@@ -1,18 +1,20 @@
1
1
  ---
2
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.
3
+ description: Use when the user wants to upload files, documents, PDFs, archives, directories, or other data to MemoryLake. Supports single files, archives (zip, tar.gz, tgz, tar, gz, 7z, tar.bz2, bz2) which are extracted then uploaded, and directories which are recursively uploaded.
4
4
  ---
5
5
 
6
6
  # MemoryLake File Upload
7
7
 
8
8
  ## Overview
9
9
 
10
- Upload local files to MemoryLake using the multipart upload API, then associate them with a project.
10
+ Upload local files, archives, or directories to MemoryLake using the multipart upload API, then associate them with a project. Archives are automatically detected, extracted, and each file inside is uploaded individually. Directories are recursively traversed and all files inside are uploaded.
11
11
 
12
12
  ## When to Use
13
13
 
14
14
  - User wants to upload a file (PDF, DOCX, image, etc.) to MemoryLake
15
15
  - User wants to add a local document to a MemoryLake project
16
+ - User wants to upload an archive (zip, tar.gz, tgz, tar, gz, 7z, tar.bz2, bz2) — files inside will be extracted and uploaded one by one
17
+ - User wants to upload an entire directory/folder — all files will be recursively collected and uploaded
16
18
 
17
19
  ## Step 1 -- Read MemoryLake Config
18
20
 
@@ -30,22 +32,39 @@ If the script exits with an error, stop and inform the user.
30
32
 
31
33
  The upload script is at `scripts/upload.mjs` relative to **this skill's SKILL.md**.
32
34
 
35
+ The script automatically detects the input type and handles it accordingly — plain files are uploaded directly, archives are extracted first, and directories are recursively traversed.
36
+
33
37
  ```bash
38
+ # Single file
34
39
  node {path-to-this-skill}/scripts/upload.mjs \
35
40
  --host {host} \
36
41
  --api-key {apiKey} \
37
42
  --project-id {projectId} \
38
43
  --file-name {fileName} \
39
44
  /path/to/file
45
+
46
+ # Archive (auto-detected, extracted, each file uploaded)
47
+ node {path-to-this-skill}/scripts/upload.mjs \
48
+ --host {host} \
49
+ --api-key {apiKey} \
50
+ --project-id {projectId} \
51
+ /path/to/archive.zip
52
+
53
+ # Directory (recursively uploads all files inside)
54
+ node {path-to-this-skill}/scripts/upload.mjs \
55
+ --host {host} \
56
+ --api-key {apiKey} \
57
+ --project-id {projectId} \
58
+ /path/to/my-folder/
40
59
  ```
41
60
 
42
- `--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.
61
+ `--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. For archives and directories, `--file-name` is not needed since individual file names are taken from the contents.
43
62
 
44
63
  ## Step 3 -- Handle Output
45
64
 
46
- The script prints progress for each step (create upload, upload parts, complete, add to project).
65
+ The script prints progress for each step (create upload, upload parts, complete, add to project). For archives and directories, it additionally prints a summary of succeeded/failed uploads.
47
66
 
48
- - **Success**: Report the document ID and file name to the user
67
+ - **Success**: Report the document ID and file name to the user. For archives/directories, report the total count and list each uploaded file
49
68
  - **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
50
69
 
51
70
  ## Common Mistakes
@@ -20,6 +20,12 @@ import fs from 'fs';
20
20
  import path from 'path';
21
21
  import https from 'https';
22
22
  import http from 'http';
23
+ import os from 'os';
24
+ import { createGunzip } from 'zlib';
25
+ import { pipeline } from 'stream/promises';
26
+ import AdmZip from 'adm-zip';
27
+ import { extract as tarExtract } from 'tar';
28
+ import sevenZip from '7zip-min';
23
29
 
24
30
  // API base path
25
31
  const API_BASE = '/openapi/memorylake';
@@ -228,6 +234,228 @@ export async function upload({ host, apiKey, projectId, filePath, fileName }) {
228
234
  return doc;
229
235
  }
230
236
 
237
+ // ============================================================================
238
+ // Archive support
239
+ // ============================================================================
240
+
241
+ /** Formats we can extract with npm packages (adm-zip, tar, zlib, 7zip-min) */
242
+ const SUPPORTED_ARCHIVE_EXTENSIONS = [
243
+ '.tar.gz', '.tgz', '.tar.bz2', '.tbz2',
244
+ '.tar', '.zip', '.gz', '.bz2', '.7z',
245
+ ];
246
+
247
+ /** Recognized but unsupported — detected so we give a clear error instead of uploading raw */
248
+ const UNSUPPORTED_ARCHIVE_EXTENSIONS = [
249
+ '.tar.xz', '.txz', '.xz', '.rar',
250
+ ];
251
+
252
+ /** All recognized archive extensions (compound first for matching priority) */
253
+ const ALL_ARCHIVE_EXTENSIONS = [...SUPPORTED_ARCHIVE_EXTENSIONS, ...UNSUPPORTED_ARCHIVE_EXTENSIONS];
254
+
255
+ /**
256
+ * Check if a file is an archive based on its extension.
257
+ * Returns the matched extension or null.
258
+ */
259
+ function getArchiveExtension(filePath) {
260
+ const lower = filePath.toLowerCase();
261
+ // Check compound extensions first (e.g. .tar.gz before .gz)
262
+ for (const ext of ALL_ARCHIVE_EXTENSIONS) {
263
+ if (lower.endsWith(ext)) return ext;
264
+ }
265
+ return null;
266
+ }
267
+
268
+ /**
269
+ * Promisified wrapper around 7zip-min's callback-based unpack API.
270
+ */
271
+ function sevenZipExtract(archivePath, destDir) {
272
+ return new Promise((resolve, reject) => {
273
+ sevenZip.unpack(archivePath, destDir, (err) => {
274
+ if (err) reject(err);
275
+ else resolve();
276
+ });
277
+ });
278
+ }
279
+
280
+ /**
281
+ * Extract an archive to the given destination directory.
282
+ * Uses npm packages (adm-zip, tar, zlib, 7zip-min) for cross-platform compatibility.
283
+ */
284
+ async function extractArchive(archivePath, destDir) {
285
+ const ext = getArchiveExtension(archivePath);
286
+
287
+ // Reject recognized-but-unsupported formats early with a clear message
288
+ if (UNSUPPORTED_ARCHIVE_EXTENSIONS.includes(ext)) {
289
+ throw new Error(`Unsupported archive format: ${ext}. Please convert to .zip, .tar.gz or .7z`);
290
+ }
291
+
292
+ console.log(` Format: ${ext}`);
293
+
294
+ switch (ext) {
295
+ case '.zip': {
296
+ const zip = new AdmZip(archivePath);
297
+ zip.extractAllTo(destDir, true);
298
+ break;
299
+ }
300
+ case '.tar.gz':
301
+ case '.tgz':
302
+ case '.tar':
303
+ await tarExtract({ file: archivePath, cwd: destDir });
304
+ break;
305
+ case '.gz': {
306
+ const baseName = path.basename(archivePath).slice(0, -3);
307
+ const destPath = path.join(destDir, baseName);
308
+ await pipeline(
309
+ fs.createReadStream(archivePath),
310
+ createGunzip(),
311
+ fs.createWriteStream(destPath),
312
+ );
313
+ break;
314
+ }
315
+ case '.tar.bz2':
316
+ case '.tbz2': {
317
+ // 7za only strips the bz2 layer, producing a .tar — need a second pass
318
+ const tmpBz2 = fs.mkdtempSync(path.join(os.tmpdir(), 'memorylake-bz2-'));
319
+ try {
320
+ await sevenZipExtract(archivePath, tmpBz2);
321
+ // Find the resulting .tar inside tmpBz2
322
+ const inner = fs.readdirSync(tmpBz2).find(f => f.endsWith('.tar'));
323
+ if (inner) {
324
+ await tarExtract({ file: path.join(tmpBz2, inner), cwd: destDir });
325
+ }
326
+ } finally {
327
+ fs.rmSync(tmpBz2, { recursive: true, force: true });
328
+ }
329
+ break;
330
+ }
331
+ case '.bz2':
332
+ case '.7z':
333
+ await sevenZipExtract(archivePath, destDir);
334
+ break;
335
+ default:
336
+ throw new Error(`Unsupported archive format: ${path.basename(archivePath)}`);
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Names and directories to skip when collecting files.
342
+ */
343
+ const IGNORED_NAMES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
344
+ const IGNORED_DIRS = new Set(['__MACOSX', '.Spotlight-V100', '.Trashes']);
345
+
346
+ /** Sanity limit to prevent accidentally uploading huge trees (e.g. node_modules) */
347
+ const MAX_COLLECT_FILES = 500;
348
+
349
+ /**
350
+ * Recursively collect all files in a directory, skipping hidden/system junk.
351
+ * Throws if the file count exceeds MAX_COLLECT_FILES.
352
+ */
353
+ function collectFiles(dir, _counter = { value: 0 }) {
354
+ const files = [];
355
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
356
+ if (IGNORED_NAMES.has(entry.name)) continue;
357
+ const full = path.join(dir, entry.name);
358
+ if (entry.isDirectory()) {
359
+ if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
360
+ files.push(...collectFiles(full, _counter));
361
+ } else if (entry.isFile()) {
362
+ _counter.value++;
363
+ if (_counter.value > MAX_COLLECT_FILES) {
364
+ throw new Error(`Too many files (>${MAX_COLLECT_FILES}). Please upload a smaller directory or archive.`);
365
+ }
366
+ files.push(full);
367
+ }
368
+ }
369
+ return files;
370
+ }
371
+
372
+ /**
373
+ * Upload multiple files with progress summary.
374
+ * @param {Array<string>} files - absolute paths of files to upload
375
+ * @param {string} label - label for the summary line (e.g. "Archive" / "Directory")
376
+ */
377
+ async function uploadMany({ host, apiKey, projectId, files, label }) {
378
+ const results = [];
379
+ let success = 0;
380
+ let failed = 0;
381
+
382
+ for (const f of files) {
383
+ try {
384
+ const doc = await upload({
385
+ host, apiKey, projectId,
386
+ filePath: f,
387
+ fileName: path.basename(f),
388
+ });
389
+ results.push(doc);
390
+ success++;
391
+ } catch (err) {
392
+ console.error(`Failed to upload ${path.basename(f)}: ${err.message}`);
393
+ failed++;
394
+ }
395
+ }
396
+
397
+ console.log(`\n${label} upload complete: ${success} succeeded, ${failed} failed.`);
398
+ return results;
399
+ }
400
+
401
+ /**
402
+ * Smart upload: automatically handles plain files, archives, and directories.
403
+ *
404
+ * - Plain file → upload directly
405
+ * - Archive → extract to temp dir, upload each extracted file
406
+ * - Directory → recursively collect files, upload each
407
+ *
408
+ * Returns a single doc object for plain files, or an array for multi-file cases.
409
+ */
410
+ export async function uploadAuto({ host, apiKey, projectId, filePath, fileName }) {
411
+ if (!fs.existsSync(filePath)) {
412
+ throw new Error(`Path not found: ${filePath}`);
413
+ }
414
+
415
+ const stat = fs.statSync(filePath);
416
+
417
+ // --- Directory ---
418
+ if (stat.isDirectory()) {
419
+ const displayName = fileName || path.basename(filePath);
420
+ console.log(`\nUploading directory: ${displayName}`);
421
+
422
+ const files = collectFiles(filePath);
423
+ if (files.length === 0) {
424
+ console.log('Directory is empty, nothing to upload.');
425
+ return [];
426
+ }
427
+ console.log(`Found ${files.length} file(s)\n`);
428
+
429
+ return uploadMany({ host, apiKey, projectId, files, label: 'Directory' });
430
+ }
431
+
432
+ // --- Archive ---
433
+ if (getArchiveExtension(filePath)) {
434
+ const displayName = fileName || path.basename(filePath);
435
+ console.log(`\nDetected archive: ${displayName}`);
436
+ console.log('Extracting...');
437
+
438
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memorylake-upload-'));
439
+ try {
440
+ await extractArchive(filePath, tmpDir);
441
+ const files = collectFiles(tmpDir);
442
+
443
+ if (files.length === 0) {
444
+ console.log('Archive is empty, nothing to upload.');
445
+ return [];
446
+ }
447
+ console.log(`Extracted ${files.length} file(s)\n`);
448
+
449
+ return await uploadMany({ host, apiKey, projectId, files, label: 'Archive' });
450
+ } finally {
451
+ fs.rmSync(tmpDir, { recursive: true, force: true });
452
+ }
453
+ }
454
+
455
+ // --- Plain file ---
456
+ return upload({ host, apiKey, projectId, filePath, fileName });
457
+ }
458
+
231
459
  // CLI entry point
232
460
  async function main() {
233
461
  const args = process.argv.slice(2);
@@ -237,17 +465,21 @@ async function main() {
237
465
  MemoryLake File Upload
238
466
 
239
467
  Usage:
240
- node upload.mjs --host <url> --api-key <key> --project-id <id> <file_path>
468
+ node upload.mjs --host <url> --api-key <key> --project-id <id> <path>
241
469
 
242
470
  Arguments:
243
471
  --host Base URL (e.g., http://10.71.10.71:3002)
244
472
  --api-key API key for authentication
245
473
  --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
474
+ --file-name Custom file name (default: basename of path)
475
+ path Path to a file, archive, or directory to upload
476
+
477
+ Archives are auto-detected and extracted; directories are recursively uploaded.
248
478
 
249
479
  Examples:
250
480
  node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 document.pdf
481
+ node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 docs.zip
482
+ node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 ./my-folder/
251
483
  `);
252
484
  process.exit(0);
253
485
  }
@@ -262,7 +494,7 @@ Examples:
262
494
  }
263
495
 
264
496
  try {
265
- await upload({ host, apiKey, projectId, filePath, fileName });
497
+ await uploadAuto({ host, apiKey, projectId, filePath, fileName });
266
498
  console.log('\nDone!\n');
267
499
  } catch (err) {
268
500
  console.error(`\nError: ${err.message}\n`);