memorylake-openclaw 1.0.2-beta.1 → 1.0.2-beta.3
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 +67 -11
- package/package.json +1 -1
- package/skills/memorylake-upload/SKILL.md +24 -5
- package/skills/memorylake-upload/scripts/upload.mjs +237 -4
package/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
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
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";
|
|
@@ -1325,7 +1325,7 @@ const memoryPlugin = {
|
|
|
1325
1325
|
name: "document_download",
|
|
1326
1326
|
label: "Document Download",
|
|
1327
1327
|
description:
|
|
1328
|
-
"Download a document (image, PDF, etc.) from MemoryLake and
|
|
1328
|
+
"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.",
|
|
1329
1329
|
parameters: Type.Object({
|
|
1330
1330
|
documentId: Type.String({
|
|
1331
1331
|
description:
|
|
@@ -1345,13 +1345,10 @@ const memoryPlugin = {
|
|
|
1345
1345
|
content: [
|
|
1346
1346
|
{
|
|
1347
1347
|
type: "text",
|
|
1348
|
-
text: `Document ${documentId}
|
|
1348
|
+
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.`,
|
|
1349
1349
|
},
|
|
1350
1350
|
],
|
|
1351
|
-
details: {
|
|
1352
|
-
media: { mediaUrl: downloadUrl },
|
|
1353
|
-
documentId,
|
|
1354
|
-
},
|
|
1351
|
+
details: { documentId },
|
|
1355
1352
|
};
|
|
1356
1353
|
} catch (err) {
|
|
1357
1354
|
return {
|
|
@@ -1684,6 +1681,65 @@ const memoryPlugin = {
|
|
|
1684
1681
|
}
|
|
1685
1682
|
});
|
|
1686
1683
|
|
|
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
|
+
|
|
1687
1743
|
memorylake
|
|
1688
1744
|
.command("stats")
|
|
1689
1745
|
.description("Show memory statistics from MemoryLake")
|
|
@@ -1760,7 +1816,7 @@ const memoryPlugin = {
|
|
|
1760
1816
|
// Auto-upload: upload inbound files to MemoryLake before prompt build
|
|
1761
1817
|
if (cfg.autoUpload) {
|
|
1762
1818
|
// Lazy-load upload function from upload.mjs
|
|
1763
|
-
let
|
|
1819
|
+
let uploadAutoFn: ((opts: { host: string; apiKey: string; projectId: string; filePath: string; fileName: string }) => Promise<unknown>) | undefined;
|
|
1764
1820
|
|
|
1765
1821
|
api.on("before_prompt_build", (event, ctx) => {
|
|
1766
1822
|
if ((ctx as any)?.trigger !== "user") {
|
|
@@ -1785,17 +1841,17 @@ const memoryPlugin = {
|
|
|
1785
1841
|
// Fire-and-forget: upload asynchronously without blocking
|
|
1786
1842
|
(async () => {
|
|
1787
1843
|
// Lazy import upload.mjs
|
|
1788
|
-
if (!
|
|
1844
|
+
if (!uploadAutoFn) {
|
|
1789
1845
|
const uploadModule = await import(
|
|
1790
1846
|
/* webpackIgnore: true */
|
|
1791
1847
|
new URL("./skills/memorylake-upload/scripts/upload.mjs", import.meta.url).href
|
|
1792
1848
|
);
|
|
1793
|
-
|
|
1849
|
+
uploadAutoFn = uploadModule.uploadAuto;
|
|
1794
1850
|
}
|
|
1795
1851
|
|
|
1796
1852
|
for (const { filePath, stat } of filesToUpload) {
|
|
1797
1853
|
try {
|
|
1798
|
-
await
|
|
1854
|
+
await uploadAutoFn!({
|
|
1799
1855
|
host: effectiveCfg.host,
|
|
1800
1856
|
apiKey: effectiveCfg.apiKey,
|
|
1801
1857
|
projectId: effectiveCfg.projectId,
|
package/package.json
CHANGED
|
@@ -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.bz2, tar, rar, 7z, etc.) 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.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
|
|
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,8 @@ 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';
|
|
23
25
|
|
|
24
26
|
// API base path
|
|
25
27
|
const API_BASE = '/openapi/memorylake';
|
|
@@ -228,6 +230,233 @@ export async function upload({ host, apiKey, projectId, filePath, fileName }) {
|
|
|
228
230
|
return doc;
|
|
229
231
|
}
|
|
230
232
|
|
|
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
|
+
|
|
231
460
|
// CLI entry point
|
|
232
461
|
async function main() {
|
|
233
462
|
const args = process.argv.slice(2);
|
|
@@ -237,17 +466,21 @@ async function main() {
|
|
|
237
466
|
MemoryLake File Upload
|
|
238
467
|
|
|
239
468
|
Usage:
|
|
240
|
-
node upload.mjs --host <url> --api-key <key> --project-id <id> <
|
|
469
|
+
node upload.mjs --host <url> --api-key <key> --project-id <id> <path>
|
|
241
470
|
|
|
242
471
|
Arguments:
|
|
243
472
|
--host Base URL (e.g., http://10.71.10.71:3002)
|
|
244
473
|
--api-key API key for authentication
|
|
245
474
|
--project-id Project ID to associate the document with (required)
|
|
246
|
-
--file-name Custom file name (default: basename of
|
|
247
|
-
|
|
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.
|
|
248
479
|
|
|
249
480
|
Examples:
|
|
250
481
|
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/
|
|
251
484
|
`);
|
|
252
485
|
process.exit(0);
|
|
253
486
|
}
|
|
@@ -262,7 +495,7 @@ Examples:
|
|
|
262
495
|
}
|
|
263
496
|
|
|
264
497
|
try {
|
|
265
|
-
await
|
|
498
|
+
await uploadAuto({ host, apiKey, projectId, filePath, fileName });
|
|
266
499
|
console.log('\nDone!\n');
|
|
267
500
|
} catch (err) {
|
|
268
501
|
console.error(`\nError: ${err.message}\n`);
|