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:
|
|
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:
|
|
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
|
+
"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
|
-
"
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
'.tar
|
|
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
|
|
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
|
-
/**
|
|
256
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
284
|
-
const ext = getArchiveExtension(archivePath);
|
|
292
|
+
console.log(` Format: ${ext}`);
|
|
285
293
|
|
|
286
294
|
switch (ext) {
|
|
287
|
-
case '.zip':
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
fs.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 '.
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
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 '.
|
|
339
|
-
runExtractCommand('unrar', ['x', '-o+', archivePath, destDir + '/']);
|
|
340
|
-
break;
|
|
331
|
+
case '.bz2':
|
|
341
332
|
case '.7z':
|
|
342
|
-
|
|
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) {
|