memorylake-openclaw 1.0.2-beta.3 → 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
@@ -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)",
@@ -1711,9 +1720,13 @@ const memoryPlugin = {
1711
1720
  console.error("No project ID configured. Use --project-id or set up agent/workspace config.");
1712
1721
  return;
1713
1722
  }
1723
+ if (!effectiveCfg.host || !effectiveCfg.apiKey) {
1724
+ console.error("Missing host or apiKey in config. Check your MemoryLake configuration.");
1725
+ return;
1726
+ }
1714
1727
 
1715
1728
  // 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>;
1729
+ let uploadFn: UploadFn;
1717
1730
  try {
1718
1731
  const uploadModule = await import(
1719
1732
  /* webpackIgnore: true */
@@ -1816,7 +1829,7 @@ const memoryPlugin = {
1816
1829
  // Auto-upload: upload inbound files to MemoryLake before prompt build
1817
1830
  if (cfg.autoUpload) {
1818
1831
  // Lazy-load upload function from upload.mjs
1819
- let uploadAutoFn: ((opts: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>) | undefined;
1832
+ let uploadAutoFn: UploadFn | undefined;
1820
1833
 
1821
1834
  api.on("before_prompt_build", (event, ctx) => {
1822
1835
  if ((ctx as any)?.trigger !== "user") {
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.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,6 +1,6 @@
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, 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
@@ -13,7 +13,7 @@ Upload local files, archives, or directories to MemoryLake using the multipart u
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
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
17
  - User wants to upload an entire directory/folder — all files will be recursively collected and uploaded
18
18
 
19
19
  ## Step 1 -- Read MemoryLake Config
@@ -21,7 +21,11 @@ import path from 'path';
21
21
  import https from 'https';
22
22
  import http from 'http';
23
23
  import os from 'os';
24
- import { execFileSync } from 'child_process';
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';
25
29
 
26
30
  // API base path
27
31
  const API_BASE = '/openapi/memorylake';
@@ -234,11 +238,20 @@ export async function upload({ host, apiKey, projectId, filePath, fileName }) {
234
238
  // Archive support
235
239
  // ============================================================================
236
240
 
237
- const ARCHIVE_EXTENSIONS = [
238
- '.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz',
239
- '.tar', '.zip', '.gz', '.bz2', '.xz', '.rar', '.7z',
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',
240
245
  ];
241
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
+
242
255
  /**
243
256
  * Check if a file is an archive based on its extension.
244
257
  * Returns the matched extension or null.
@@ -246,100 +259,78 @@ const ARCHIVE_EXTENSIONS = [
246
259
  function getArchiveExtension(filePath) {
247
260
  const lower = filePath.toLowerCase();
248
261
  // Check compound extensions first (e.g. .tar.gz before .gz)
249
- for (const ext of ARCHIVE_EXTENSIONS) {
262
+ for (const ext of ALL_ARCHIVE_EXTENSIONS) {
250
263
  if (lower.endsWith(ext)) return ext;
251
264
  }
252
265
  return null;
253
266
  }
254
267
 
255
- /** Max buffer size for single-file decompression (gz/bz2/xz) stdout capture: 512 MB */
256
- const DECOMPRESS_MAX_BUFFER = 512 * 1024 * 1024;
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
+ }
257
279
 
258
280
  /**
259
281
  * Extract an archive to the given destination directory.
282
+ * Uses npm packages (adm-zip, tar, zlib, 7zip-min) for cross-platform compatibility.
260
283
  */
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;
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`);
280
290
  }
281
- }
282
291
 
283
- function extractArchive(archivePath, destDir) {
284
- const ext = getArchiveExtension(archivePath);
292
+ console.log(` Format: ${ext}`);
285
293
 
286
294
  switch (ext) {
287
- case '.zip':
288
- runExtractCommand('unzip', ['-o', archivePath, '-d', destDir]);
295
+ case '.zip': {
296
+ const zip = new AdmZip(archivePath);
297
+ zip.extractAllTo(destDir, true);
289
298
  break;
299
+ }
290
300
  case '.tar.gz':
291
301
  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
302
  case '.tar':
303
- runExtractCommand('tar', ['-xf', archivePath, '-C', destDir]);
303
+ await tarExtract({ file: archivePath, cwd: destDir });
304
304
  break;
305
305
  case '.gz': {
306
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
- }
307
+ const destPath = path.join(destDir, baseName);
308
+ await pipeline(
309
+ fs.createReadStream(archivePath),
310
+ createGunzip(),
311
+ fs.createWriteStream(destPath),
312
+ );
325
313
  break;
326
314
  }
327
- case '.xz': {
328
- const baseName = path.basename(archivePath).slice(0, -3);
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-'));
329
319
  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;
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 });
335
328
  }
336
329
  break;
337
330
  }
338
- case '.rar':
339
- runExtractCommand('unrar', ['x', '-o+', archivePath, destDir + '/']);
340
- break;
331
+ case '.bz2':
341
332
  case '.7z':
342
- runExtractCommand('7z', ['x', archivePath, `-o${destDir}`, '-y']);
333
+ await sevenZipExtract(archivePath, destDir);
343
334
  break;
344
335
  default:
345
336
  throw new Error(`Unsupported archive format: ${path.basename(archivePath)}`);
@@ -352,18 +343,26 @@ function extractArchive(archivePath, destDir) {
352
343
  const IGNORED_NAMES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
353
344
  const IGNORED_DIRS = new Set(['__MACOSX', '.Spotlight-V100', '.Trashes']);
354
345
 
346
+ /** Sanity limit to prevent accidentally uploading huge trees (e.g. node_modules) */
347
+ const MAX_COLLECT_FILES = 500;
348
+
355
349
  /**
356
350
  * Recursively collect all files in a directory, skipping hidden/system junk.
351
+ * Throws if the file count exceeds MAX_COLLECT_FILES.
357
352
  */
358
- function collectFiles(dir) {
353
+ function collectFiles(dir, _counter = { value: 0 }) {
359
354
  const files = [];
360
355
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
361
356
  if (IGNORED_NAMES.has(entry.name)) continue;
362
357
  const full = path.join(dir, entry.name);
363
358
  if (entry.isDirectory()) {
364
359
  if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
365
- files.push(...collectFiles(full));
360
+ files.push(...collectFiles(full, _counter));
366
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
+ }
367
366
  files.push(full);
368
367
  }
369
368
  }
@@ -438,7 +437,7 @@ export async function uploadAuto({ host, apiKey, projectId, filePath, fileName }
438
437
 
439
438
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'memorylake-upload-'));
440
439
  try {
441
- extractArchive(filePath, tmpDir);
440
+ await extractArchive(filePath, tmpDir);
442
441
  const files = collectFiles(tmpDir);
443
442
 
444
443
  if (files.length === 0) {