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 +79 -24
- package/package.json +5 -2
- package/skills/memorylake-upload/SKILL.md +24 -5
- package/skills/memorylake-upload/scripts/upload.mjs +236 -4
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
|
|
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: {
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
1862
|
+
uploadAutoFn = uploadModule.uploadAuto;
|
|
1808
1863
|
}
|
|
1809
1864
|
|
|
1810
1865
|
for (const { filePath, stat } of filesToUpload) {
|
|
1811
1866
|
try {
|
|
1812
|
-
await
|
|
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.
|
|
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,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
|
|
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> <
|
|
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
|
|
247
|
-
|
|
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
|
|
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`);
|