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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 relevant memories and document excerpts before each agent turn
8
+ * - Auto-recall: injects memory-search instructions (+ open-data categories) into the system prompt 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, openclaw memorylake upload
10
+ * - CLI: openclaw memorylake search, openclaw memorylake stats
11
11
  */
12
12
 
13
13
  import fs from "node:fs";
@@ -994,6 +994,23 @@ const memoryPlugin = {
994
994
  content: r.content,
995
995
  created_at: r.created_at,
996
996
  }));
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
+ }
997
1014
  } else if (memoryResult.status === "rejected") {
998
1015
  sections.push(`## Memories\nMemory search failed: ${String(memoryResult.reason)}`);
999
1016
  }
@@ -1011,7 +1028,7 @@ const memoryPlugin = {
1011
1028
  content: [
1012
1029
  { type: "text", text: "No relevant memories or documents found." },
1013
1030
  ],
1014
- details: { count: 0 },
1031
+ details: { memoryCount: 0, documentCount: 0, memories: [] },
1015
1032
  };
1016
1033
  }
1017
1034
 
@@ -1681,65 +1698,6 @@ const memoryPlugin = {
1681
1698
  }
1682
1699
  });
1683
1700
 
1684
- memorylake
1685
- .command("upload")
1686
- .description("Upload files or directories to MemoryLake")
1687
- .argument("<path>", "File or directory path to upload")
1688
- .option("--agent <id>", "Agent ID (resolves workspace and per-agent projectId)")
1689
- .option("--project-id <id>", "Override project ID (takes precedence over --agent)")
1690
- .action(async (targetPath: string, opts: { agent?: string; projectId?: string }) => {
1691
- // Resolve effective config: --project-id > agent workspace config > global config
1692
- let effectiveCfg = cfg;
1693
- if (opts.agent) {
1694
- try {
1695
- const openclawPath = path.join(os.homedir(), ".openclaw", "openclaw.json");
1696
- const openclaw = JSON.parse(fs.readFileSync(openclawPath, "utf-8"));
1697
- const agents = openclaw?.agents;
1698
- const agentEntry = agents?.list?.find((a: any) => a.id === opts.agent);
1699
- const workspace = agentEntry?.workspace || agents?.defaults?.workspace;
1700
- if (workspace) {
1701
- effectiveCfg = resolveConfig({ workspaceDir: workspace });
1702
- } else {
1703
- console.warn(`Warning: no workspace found for agent "${opts.agent}", using global config.`);
1704
- }
1705
- } catch (err) {
1706
- console.warn(`Warning: failed to resolve agent config: ${String(err)}, using global config.`);
1707
- }
1708
- }
1709
- const effectiveProjectId = opts.projectId || effectiveCfg.projectId;
1710
- if (!effectiveProjectId) {
1711
- console.error("No project ID configured. Use --project-id or set up agent/workspace config.");
1712
- return;
1713
- }
1714
-
1715
- // Lazy import upload.mjs (use uploadAuto to support archives)
1716
- let uploadFn: (o: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>;
1717
- try {
1718
- const uploadModule = await import(
1719
- /* webpackIgnore: true */
1720
- new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
1721
- );
1722
- uploadFn = uploadModule.uploadAuto;
1723
- } catch (err) {
1724
- console.error(`Failed to load upload module: ${String(err)}`);
1725
- return;
1726
- }
1727
-
1728
- const absPath = path.resolve(targetPath);
1729
-
1730
- try {
1731
- await uploadFn({
1732
- host: effectiveCfg.host,
1733
- apiKey: effectiveCfg.apiKey,
1734
- projectId: effectiveProjectId,
1735
- filePath: absPath,
1736
- fileName: path.basename(absPath),
1737
- });
1738
- } catch (err) {
1739
- console.error(`Upload failed: ${String(err)}`);
1740
- }
1741
- });
1742
-
1743
1701
  memorylake
1744
1702
  .command("stats")
1745
1703
  .description("Show memory statistics from MemoryLake")
@@ -1816,7 +1774,7 @@ const memoryPlugin = {
1816
1774
  // Auto-upload: upload inbound files to MemoryLake before prompt build
1817
1775
  if (cfg.autoUpload) {
1818
1776
  // Lazy-load upload function from upload.mjs
1819
- let uploadAutoFn: ((opts: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>) | undefined;
1777
+ let uploadFn: ((opts: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>) | undefined;
1820
1778
 
1821
1779
  api.on("before_prompt_build", (event, ctx) => {
1822
1780
  if ((ctx as any)?.trigger !== "user") {
@@ -1841,17 +1799,17 @@ const memoryPlugin = {
1841
1799
  // Fire-and-forget: upload asynchronously without blocking
1842
1800
  (async () => {
1843
1801
  // Lazy import upload.mjs
1844
- if (!uploadAutoFn) {
1802
+ if (!uploadFn) {
1845
1803
  const uploadModule = await import(
1846
1804
  /* webpackIgnore: true */
1847
1805
  new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
1848
1806
  );
1849
- uploadAutoFn = uploadModule.uploadAuto;
1807
+ uploadFn = uploadModule.upload;
1850
1808
  }
1851
1809
 
1852
1810
  for (const { filePath, stat } of filesToUpload) {
1853
1811
  try {
1854
- await uploadAutoFn!({
1812
+ await uploadFn!({
1855
1813
  host: effectiveCfg.host,
1856
1814
  apiKey: effectiveCfg.apiKey,
1857
1815
  projectId: effectiveCfg.projectId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "1.0.2-beta.3",
3
+ "version": "1.0.2-beta.4",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",
@@ -1,20 +1,18 @@
1
1
  ---
2
2
  name: memorylake-upload
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.bz2, tar, rar, 7z, etc.) which are extracted then uploaded, and directories which are recursively uploaded.
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
4
  ---
5
5
 
6
6
  # MemoryLake File Upload
7
7
 
8
8
  ## Overview
9
9
 
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.
10
+ Upload local files to MemoryLake using the multipart upload API, then associate them with a project.
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.bz2, tbz2, tar.xz, txz, tar, gz, bz2, xz, rar, 7z) — 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
18
16
 
19
17
  ## Step 1 -- Read MemoryLake Config
20
18
 
@@ -32,39 +30,22 @@ If the script exits with an error, stop and inform the user.
32
30
 
33
31
  The upload script is at `scripts/upload.mjs` relative to **this skill's SKILL.md**.
34
32
 
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
-
37
33
  ```bash
38
- # Single file
39
34
  node {path-to-this-skill}/scripts/upload.mjs \
40
35
  --host {host} \
41
36
  --api-key {apiKey} \
42
37
  --project-id {projectId} \
43
38
  --file-name {fileName} \
44
39
  /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/
59
40
  ```
60
41
 
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.
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.
62
43
 
63
44
  ## Step 3 -- Handle Output
64
45
 
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.
46
+ The script prints progress for each step (create upload, upload parts, complete, add to project).
66
47
 
67
- - **Success**: Report the document ID and file name to the user. For archives/directories, report the total count and list each uploaded file
48
+ - **Success**: Report the document ID and file name to the user
68
49
  - **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
69
50
 
70
51
  ## Common Mistakes
@@ -20,8 +20,6 @@ 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 { execFileSync } from 'child_process';
25
23
 
26
24
  // API base path
27
25
  const API_BASE = '/openapi/memorylake';
@@ -230,233 +228,6 @@ export async function upload({ host, apiKey, projectId, filePath, fileName }) {
230
228
  return doc;
231
229
  }
232
230
 
233
- // ============================================================================
234
- // Archive support
235
- // ============================================================================
236
-
237
- const ARCHIVE_EXTENSIONS = [
238
- '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz',
239
- '.tar', '.zip', '.gz', '.bz2', '.xz', '.rar', '.7z',
240
- ];
241
-
242
- /**
243
- * Check if a file is an archive based on its extension.
244
- * Returns the matched extension or null.
245
- */
246
- function getArchiveExtension(filePath) {
247
- const lower = filePath.toLowerCase();
248
- // Check compound extensions first (e.g. .tar.gz before .gz)
249
- for (const ext of ARCHIVE_EXTENSIONS) {
250
- if (lower.endsWith(ext)) return ext;
251
- }
252
- return null;
253
- }
254
-
255
- /** Max buffer size for single-file decompression (gz/bz2/xz) stdout capture: 512 MB */
256
- const DECOMPRESS_MAX_BUFFER = 512 * 1024 * 1024;
257
-
258
- /**
259
- * Extract an archive to the given destination directory.
260
- */
261
- const TOOL_INSTALL_HINTS = {
262
- unzip: 'Install via: sudo apt install unzip (Linux) / brew install unzip (macOS) / choco install unzip (Windows)',
263
- tar: 'tar should be pre-installed on most systems (Linux/macOS/Windows 10+)',
264
- gunzip: 'Install via: sudo apt install gzip (Linux) / brew install gzip (macOS) / choco install gzip (Windows)',
265
- bunzip2: 'Install via: sudo apt install bzip2 (Linux) / brew install bzip2 (macOS) / choco install bzip2 (Windows)',
266
- xz: 'Install via: sudo apt install xz-utils (Linux) / brew install xz (macOS) / choco install xz (Windows)',
267
- unrar: 'Install via: sudo apt install unrar (Linux) / brew install unrar (macOS) / choco install unrar (Windows)',
268
- '7z': 'Install via: sudo apt install p7zip-full (Linux) / brew install 7zip (macOS) / choco install 7zip (Windows)',
269
- };
270
-
271
- function runExtractCommand(tool, args) {
272
- try {
273
- execFileSync(tool, args, { stdio: 'pipe' });
274
- } catch (err) {
275
- if (err.code === 'ENOENT') {
276
- const hint = TOOL_INSTALL_HINTS[tool] || '';
277
- throw new Error(`"${tool}" not found. ${hint}`);
278
- }
279
- throw err;
280
- }
281
- }
282
-
283
- function extractArchive(archivePath, destDir) {
284
- const ext = getArchiveExtension(archivePath);
285
-
286
- switch (ext) {
287
- case '.zip':
288
- runExtractCommand('unzip', ['-o', archivePath, '-d', destDir]);
289
- break;
290
- case '.tar.gz':
291
- case '.tgz':
292
- runExtractCommand('tar', ['-xzf', archivePath, '-C', destDir]);
293
- break;
294
- case '.tar.bz2':
295
- case '.tbz2':
296
- runExtractCommand('tar', ['-xjf', archivePath, '-C', destDir]);
297
- break;
298
- case '.tar.xz':
299
- case '.txz':
300
- runExtractCommand('tar', ['-xJf', archivePath, '-C', destDir]);
301
- break;
302
- case '.tar':
303
- runExtractCommand('tar', ['-xf', archivePath, '-C', destDir]);
304
- break;
305
- case '.gz': {
306
- const baseName = path.basename(archivePath).slice(0, -3);
307
- try {
308
- const output = execFileSync('gunzip', ['-c', archivePath], { maxBuffer: DECOMPRESS_MAX_BUFFER });
309
- fs.writeFileSync(path.join(destDir, baseName), output);
310
- } catch (err) {
311
- if (err.code === 'ENOENT') throw new Error(`"gunzip" not found. ${TOOL_INSTALL_HINTS.gunzip}`);
312
- throw err;
313
- }
314
- break;
315
- }
316
- case '.bz2': {
317
- const baseName = path.basename(archivePath).slice(0, -4);
318
- try {
319
- const output = execFileSync('bunzip2', ['-c', archivePath], { maxBuffer: DECOMPRESS_MAX_BUFFER });
320
- fs.writeFileSync(path.join(destDir, baseName), output);
321
- } catch (err) {
322
- if (err.code === 'ENOENT') throw new Error(`"bunzip2" not found. ${TOOL_INSTALL_HINTS.bunzip2}`);
323
- throw err;
324
- }
325
- break;
326
- }
327
- case '.xz': {
328
- const baseName = path.basename(archivePath).slice(0, -3);
329
- try {
330
- const output = execFileSync('xz', ['-dc', archivePath], { maxBuffer: DECOMPRESS_MAX_BUFFER });
331
- fs.writeFileSync(path.join(destDir, baseName), output);
332
- } catch (err) {
333
- if (err.code === 'ENOENT') throw new Error(`"xz" not found. ${TOOL_INSTALL_HINTS.xz}`);
334
- throw err;
335
- }
336
- break;
337
- }
338
- case '.rar':
339
- runExtractCommand('unrar', ['x', '-o+', archivePath, destDir + '/']);
340
- break;
341
- case '.7z':
342
- runExtractCommand('7z', ['x', archivePath, `-o${destDir}`, '-y']);
343
- break;
344
- default:
345
- throw new Error(`Unsupported archive format: ${path.basename(archivePath)}`);
346
- }
347
- }
348
-
349
- /**
350
- * Names and directories to skip when collecting files.
351
- */
352
- const IGNORED_NAMES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
353
- const IGNORED_DIRS = new Set(['__MACOSX', '.Spotlight-V100', '.Trashes']);
354
-
355
- /**
356
- * Recursively collect all files in a directory, skipping hidden/system junk.
357
- */
358
- function collectFiles(dir) {
359
- const files = [];
360
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
361
- if (IGNORED_NAMES.has(entry.name)) continue;
362
- const full = path.join(dir, entry.name);
363
- if (entry.isDirectory()) {
364
- if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
365
- files.push(...collectFiles(full));
366
- } else if (entry.isFile()) {
367
- files.push(full);
368
- }
369
- }
370
- return files;
371
- }
372
-
373
- /**
374
- * Upload multiple files with progress summary.
375
- * @param {Array<string>} files - absolute paths of files to upload
376
- * @param {string} label - label for the summary line (e.g. "Archive" / "Directory")
377
- */
378
- async function uploadMany({ host, apiKey, projectId, files, label }) {
379
- const results = [];
380
- let success = 0;
381
- let failed = 0;
382
-
383
- for (const f of files) {
384
- try {
385
- const doc = await upload({
386
- host, apiKey, projectId,
387
- filePath: f,
388
- fileName: path.basename(f),
389
- });
390
- results.push(doc);
391
- success++;
392
- } catch (err) {
393
- console.error(`Failed to upload ${path.basename(f)}: ${err.message}`);
394
- failed++;
395
- }
396
- }
397
-
398
- console.log(`\n${label} upload complete: ${success} succeeded, ${failed} failed.`);
399
- return results;
400
- }
401
-
402
- /**
403
- * Smart upload: automatically handles plain files, archives, and directories.
404
- *
405
- * - Plain file → upload directly
406
- * - Archive → extract to temp dir, upload each extracted file
407
- * - Directory → recursively collect files, upload each
408
- *
409
- * Returns a single doc object for plain files, or an array for multi-file cases.
410
- */
411
- export async function uploadAuto({ host, apiKey, projectId, filePath, fileName }) {
412
- if (!fs.existsSync(filePath)) {
413
- throw new Error(`Path not found: ${filePath}`);
414
- }
415
-
416
- const stat = fs.statSync(filePath);
417
-
418
- // --- Directory ---
419
- if (stat.isDirectory()) {
420
- const displayName = fileName || path.basename(filePath);
421
- console.log(`\nUploading directory: ${displayName}`);
422
-
423
- const files = collectFiles(filePath);
424
- if (files.length === 0) {
425
- console.log('Directory is empty, nothing to upload.');
426
- return [];
427
- }
428
- console.log(`Found ${files.length} file(s)\n`);
429
-
430
- return uploadMany({ host, apiKey, projectId, files, label: 'Directory' });
431
- }
432
-
433
- // --- Archive ---
434
- if (getArchiveExtension(filePath)) {
435
- const displayName = fileName || path.basename(filePath);
436
- console.log(`\nDetected archive: ${displayName}`);
437
- console.log('Extracting...');
438
-
439
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memorylake-upload-'));
440
- try {
441
- extractArchive(filePath, tmpDir);
442
- const files = collectFiles(tmpDir);
443
-
444
- if (files.length === 0) {
445
- console.log('Archive is empty, nothing to upload.');
446
- return [];
447
- }
448
- console.log(`Extracted ${files.length} file(s)\n`);
449
-
450
- return await uploadMany({ host, apiKey, projectId, files, label: 'Archive' });
451
- } finally {
452
- fs.rmSync(tmpDir, { recursive: true, force: true });
453
- }
454
- }
455
-
456
- // --- Plain file ---
457
- return upload({ host, apiKey, projectId, filePath, fileName });
458
- }
459
-
460
231
  // CLI entry point
461
232
  async function main() {
462
233
  const args = process.argv.slice(2);
@@ -466,21 +237,17 @@ async function main() {
466
237
  MemoryLake File Upload
467
238
 
468
239
  Usage:
469
- node upload.mjs --host <url> --api-key <key> --project-id <id> <path>
240
+ node upload.mjs --host <url> --api-key <key> --project-id <id> <file_path>
470
241
 
471
242
  Arguments:
472
243
  --host Base URL (e.g., http://10.71.10.71:3002)
473
244
  --api-key API key for authentication
474
245
  --project-id Project ID to associate the document with (required)
475
- --file-name Custom file name (default: basename of path)
476
- path Path to a file, archive, or directory to upload
477
-
478
- Archives are auto-detected and extracted; directories are recursively uploaded.
246
+ --file-name Custom file name (default: basename of file_path)
247
+ file_path Path to the file to upload
479
248
 
480
249
  Examples:
481
250
  node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 document.pdf
482
- node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 docs.zip
483
- node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 ./my-folder/
484
251
  `);
485
252
  process.exit(0);
486
253
  }
@@ -495,7 +262,7 @@ Examples:
495
262
  }
496
263
 
497
264
  try {
498
- await uploadAuto({ host, apiKey, projectId, filePath, fileName });
265
+ await upload({ host, apiKey, projectId, filePath, fileName });
499
266
  console.log('\nDone!\n');
500
267
  } catch (err) {
501
268
  console.error(`\nError: ${err.message}\n`);