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

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
@@ -15,6 +15,7 @@ import fsPromises from "node:fs/promises";
15
15
  import os from "node:os";
16
16
  import path from "node:path";
17
17
  import got from "got";
18
+ import { pipeline } from "node:stream/promises";
18
19
  import { Type } from "@sinclair/typebox";
19
20
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
20
21
  import { loadCoreAgentDeps } from "./core-bridge";
@@ -1329,35 +1330,76 @@ const memoryPlugin = {
1329
1330
  { name: "document_search" },
1330
1331
  );
1331
1332
 
1333
+ /**
1334
+ * Try to extract a usable filename from a pre-signed download URL.
1335
+ * Returns null if the URL doesn't contain a recognizable filename.
1336
+ */
1337
+ function fileNameFromUrl(urlStr: string): string | null {
1338
+ try {
1339
+ const p = new URL(urlStr).pathname;
1340
+ const base = path.posix.basename(p);
1341
+ if (base && /\.\w{1,10}$/.test(base)) return decodeURIComponent(base);
1342
+ } catch { /* ignore */ }
1343
+ return null;
1344
+ }
1345
+
1332
1346
  api.registerTool(
1333
1347
  (ctx) => ({
1334
1348
  name: "document_download",
1335
1349
  label: "Document Download",
1336
1350
  description:
1337
- "Download a document (image, PDF, etc.) from MemoryLake and get a temporary download URL. After calling this tool, you MUST call the `message` tool with action='send' and media=<the returned URL> to deliver the file to the user.",
1351
+ "Download a document (image, PDF, etc.) from MemoryLake to local disk. After calling this tool, you MUST call the `message` tool with action='send' and media=<the returned local file path> to deliver the file to the user.",
1338
1352
  parameters: Type.Object({
1339
1353
  documentId: Type.String({
1340
1354
  description:
1341
1355
  "The document ID to download (from document_search results or document listing)",
1342
1356
  }),
1357
+ fileName: Type.Optional(
1358
+ Type.String({
1359
+ description:
1360
+ "Original file name for saving locally (e.g. 'report.pdf'). Falls back to the name in the download URL or the document ID.",
1361
+ }),
1362
+ ),
1343
1363
  }),
1344
1364
  async execute(_toolCallId, params) {
1345
1365
  const effectiveCfg = resolveConfig(ctx);
1346
1366
  const effectiveProvider = getProvider(effectiveCfg);
1347
- const { documentId } = params as { documentId: string };
1367
+ const { documentId, fileName } = params as {
1368
+ documentId: string;
1369
+ fileName?: string;
1370
+ };
1348
1371
 
1349
1372
  try {
1373
+ // 1. Get pre-signed download URL
1350
1374
  const downloadUrl =
1351
1375
  await effectiveProvider.getDocumentDownloadUrl(documentId);
1352
1376
 
1377
+ // 2. Determine local save directory (cross-platform)
1378
+ const workspaceDir = (ctx as any)?.workspaceDir;
1379
+ const downloadDir = workspaceDir
1380
+ ? path.join(workspaceDir, ".memorylake", "downloads")
1381
+ : path.join(os.tmpdir(), "memorylake-downloads");
1382
+ fs.mkdirSync(downloadDir, { recursive: true });
1383
+
1384
+ // 3. Determine filename: explicit param > URL-derived > documentId
1385
+ const finalName =
1386
+ fileName || fileNameFromUrl(downloadUrl) || documentId;
1387
+ const localPath = path.join(downloadDir, finalName);
1388
+
1389
+ // 4. Stream download to local file
1390
+ await pipeline(
1391
+ got.stream(downloadUrl),
1392
+ fs.createWriteStream(localPath),
1393
+ );
1394
+
1353
1395
  return {
1354
1396
  content: [
1355
1397
  {
1356
1398
  type: "text",
1357
- text: `Document ${documentId} ready. Download URL (expires in ~20 minutes):\n${downloadUrl}\n\nYou MUST now call the message tool with action="send" and media set to this URL to deliver the file to the user.`,
1399
+ text: `Document ${documentId} downloaded to:\n${localPath}\n\nYou MUST now call the message tool with action="send" and media set to this local path to deliver the file to the user.`,
1358
1400
  },
1359
1401
  ],
1360
- details: { documentId },
1402
+ details: { documentId, localPath },
1361
1403
  };
1362
1404
  } catch (err) {
1363
1405
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "1.0.2-beta.5",
3
+ "version": "1.0.2-beta.6",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",
@@ -369,8 +369,13 @@ function collectFiles(dir, _counter = { value: 0 }) {
369
369
  return files;
370
370
  }
371
371
 
372
+ /** Max number of concurrent uploads */
373
+ const UPLOAD_CONCURRENCY = 10;
374
+
372
375
  /**
373
- * Upload multiple files with progress summary.
376
+ * Upload multiple files with a sliding-window concurrency pool.
377
+ * Up to UPLOAD_CONCURRENCY uploads run simultaneously; as soon as one
378
+ * finishes the next file starts immediately — no waiting for a full batch.
374
379
  * @param {Array<string>} files - absolute paths of files to upload
375
380
  * @param {string} label - label for the summary line (e.g. "Archive" / "Directory")
376
381
  */
@@ -378,21 +383,31 @@ async function uploadMany({ host, apiKey, projectId, files, label }) {
378
383
  const results = [];
379
384
  let success = 0;
380
385
  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
- });
386
+ let nextIndex = 0;
387
+
388
+ function runNext() {
389
+ if (nextIndex >= files.length) return null;
390
+ const i = nextIndex++;
391
+ const f = files[i];
392
+ return upload({
393
+ host, apiKey, projectId,
394
+ filePath: f,
395
+ fileName: path.basename(f),
396
+ }).then(doc => {
389
397
  results.push(doc);
390
398
  success++;
391
- } catch (err) {
399
+ }).catch(err => {
392
400
  console.error(`Failed to upload ${path.basename(f)}: ${err.message}`);
393
401
  failed++;
394
- }
402
+ }).then(() => runNext()); // slot freed → pick up next file immediately
403
+ }
404
+
405
+ // Kick off the initial pool of workers
406
+ const workers = [];
407
+ for (let w = 0; w < Math.min(UPLOAD_CONCURRENCY, files.length); w++) {
408
+ workers.push(runNext());
395
409
  }
410
+ await Promise.all(workers);
396
411
 
397
412
  console.log(`\n${label} upload complete: ${success} succeeded, ${failed} failed.`);
398
413
  return results;