memorylake-openclaw 0.0.6 → 0.0.8
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
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* - CLI: openclaw memorylake search, openclaw memorylake stats
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
13
15
|
import got from "got";
|
|
14
16
|
import { Type } from "@sinclair/typebox";
|
|
15
17
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
@@ -530,6 +532,52 @@ const memoryPlugin = {
|
|
|
530
532
|
const cfg = memoryLakeConfigSchema.parse(api.pluginConfig);
|
|
531
533
|
const provider: MemoryLakeProvider = new PlatformProvider(cfg.host, cfg.apiKey, cfg.projectId);
|
|
532
534
|
|
|
535
|
+
// Provider cache: avoids re-creating providers for the same host+apiKey+projectId
|
|
536
|
+
const providerCache = new Map<string, MemoryLakeProvider>();
|
|
537
|
+
const globalProviderKey = `${cfg.host}|${cfg.apiKey}|${cfg.projectId}`;
|
|
538
|
+
providerCache.set(globalProviderKey, provider);
|
|
539
|
+
|
|
540
|
+
function getProvider(effectiveCfg: MemoryLakeConfig): MemoryLakeProvider {
|
|
541
|
+
const key = `${effectiveCfg.host}|${effectiveCfg.apiKey}|${effectiveCfg.projectId}`;
|
|
542
|
+
let p = providerCache.get(key);
|
|
543
|
+
if (!p) {
|
|
544
|
+
p = new PlatformProvider(effectiveCfg.host, effectiveCfg.apiKey, effectiveCfg.projectId);
|
|
545
|
+
providerCache.set(key, p);
|
|
546
|
+
}
|
|
547
|
+
return p;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function resolveConfig(ctx: any): MemoryLakeConfig {
|
|
551
|
+
const workspaceDir = ctx?.workspaceDir;
|
|
552
|
+
if (!workspaceDir) return cfg;
|
|
553
|
+
|
|
554
|
+
const localPath = path.join(workspaceDir, ".memorylake", "config.json");
|
|
555
|
+
if (!fs.existsSync(localPath)) return cfg;
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const raw = JSON.parse(fs.readFileSync(localPath, "utf-8"));
|
|
559
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
560
|
+
api.logger.warn(
|
|
561
|
+
`memorylake-openclaw: workspace config exists but is not a JSON object; falling back to global config (path: ${localPath})`,
|
|
562
|
+
);
|
|
563
|
+
return cfg;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const allowed = new Set(ALLOWED_KEYS);
|
|
567
|
+
const overrides: Record<string, unknown> = {};
|
|
568
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
569
|
+
if (allowed.has(key)) overrides[key] = value;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return { ...cfg, ...overrides } as MemoryLakeConfig;
|
|
573
|
+
} catch {
|
|
574
|
+
api.logger.warn(
|
|
575
|
+
`memorylake-openclaw: failed to parse workspace config JSON; falling back to global config (path: ${localPath})`,
|
|
576
|
+
);
|
|
577
|
+
return cfg;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
533
581
|
// Track current session ID for tool-level session scoping
|
|
534
582
|
let currentSessionId: string | undefined;
|
|
535
583
|
|
|
@@ -538,9 +586,9 @@ const memoryPlugin = {
|
|
|
538
586
|
);
|
|
539
587
|
|
|
540
588
|
// Helper: build add options
|
|
541
|
-
function buildAddOptions(userIdOverride?: string, sessionId?: string): AddOptions {
|
|
589
|
+
function buildAddOptions(effectiveCfg: MemoryLakeConfig, userIdOverride?: string, sessionId?: string): AddOptions {
|
|
542
590
|
const opts: AddOptions = {
|
|
543
|
-
user_id: userIdOverride ||
|
|
591
|
+
user_id: userIdOverride || effectiveCfg.userId,
|
|
544
592
|
infer: true,
|
|
545
593
|
metadata: { source: "OPENCLAW" },
|
|
546
594
|
};
|
|
@@ -550,14 +598,15 @@ const memoryPlugin = {
|
|
|
550
598
|
|
|
551
599
|
// Helper: build search options
|
|
552
600
|
function buildSearchOptions(
|
|
601
|
+
effectiveCfg: MemoryLakeConfig,
|
|
553
602
|
userIdOverride?: string,
|
|
554
603
|
limit?: number,
|
|
555
604
|
): SearchOptions {
|
|
556
605
|
return {
|
|
557
|
-
user_id: userIdOverride ||
|
|
558
|
-
top_k: limit ??
|
|
559
|
-
threshold:
|
|
560
|
-
rerank:
|
|
606
|
+
user_id: userIdOverride || effectiveCfg.userId,
|
|
607
|
+
top_k: limit ?? effectiveCfg.topK,
|
|
608
|
+
threshold: effectiveCfg.searchThreshold,
|
|
609
|
+
rerank: effectiveCfg.rerank,
|
|
561
610
|
};
|
|
562
611
|
}
|
|
563
612
|
|
|
@@ -566,7 +615,7 @@ const memoryPlugin = {
|
|
|
566
615
|
// ========================================================================
|
|
567
616
|
|
|
568
617
|
api.registerTool(
|
|
569
|
-
{
|
|
618
|
+
(ctx) => ({
|
|
570
619
|
name: "memory_search",
|
|
571
620
|
label: "Memory Search",
|
|
572
621
|
description:
|
|
@@ -596,6 +645,8 @@ const memoryPlugin = {
|
|
|
596
645
|
),
|
|
597
646
|
}),
|
|
598
647
|
async execute(_toolCallId, params) {
|
|
648
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
649
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
599
650
|
const { query, limit, userId, scope = "all" } = params as {
|
|
600
651
|
query: string;
|
|
601
652
|
limit?: number;
|
|
@@ -604,9 +655,9 @@ const memoryPlugin = {
|
|
|
604
655
|
};
|
|
605
656
|
|
|
606
657
|
try {
|
|
607
|
-
const results = await
|
|
658
|
+
const results = await effectiveProvider.search(
|
|
608
659
|
query,
|
|
609
|
-
buildSearchOptions(userId, limit),
|
|
660
|
+
buildSearchOptions(effectiveCfg, userId, limit),
|
|
610
661
|
);
|
|
611
662
|
|
|
612
663
|
if (!results || results.length === 0) {
|
|
@@ -652,12 +703,12 @@ const memoryPlugin = {
|
|
|
652
703
|
};
|
|
653
704
|
}
|
|
654
705
|
},
|
|
655
|
-
},
|
|
706
|
+
}),
|
|
656
707
|
{ name: "memory_search" },
|
|
657
708
|
);
|
|
658
709
|
|
|
659
710
|
api.registerTool(
|
|
660
|
-
{
|
|
711
|
+
(ctx) => ({
|
|
661
712
|
name: "memory_store",
|
|
662
713
|
label: "Memory Store",
|
|
663
714
|
description:
|
|
@@ -676,6 +727,8 @@ const memoryPlugin = {
|
|
|
676
727
|
),
|
|
677
728
|
}),
|
|
678
729
|
async execute(_toolCallId, params) {
|
|
730
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
731
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
679
732
|
const { text, userId } = params as {
|
|
680
733
|
text: string;
|
|
681
734
|
userId?: string;
|
|
@@ -683,9 +736,9 @@ const memoryPlugin = {
|
|
|
683
736
|
};
|
|
684
737
|
|
|
685
738
|
try {
|
|
686
|
-
const result = await
|
|
739
|
+
const result = await effectiveProvider.add(
|
|
687
740
|
[{ role: "user", content: text }],
|
|
688
|
-
buildAddOptions(userId, currentSessionId),
|
|
741
|
+
buildAddOptions(effectiveCfg, userId, currentSessionId),
|
|
689
742
|
);
|
|
690
743
|
|
|
691
744
|
const count = result.results?.length ?? 0;
|
|
@@ -716,12 +769,12 @@ const memoryPlugin = {
|
|
|
716
769
|
};
|
|
717
770
|
}
|
|
718
771
|
},
|
|
719
|
-
},
|
|
772
|
+
}),
|
|
720
773
|
{ name: "memory_store" },
|
|
721
774
|
);
|
|
722
775
|
|
|
723
776
|
api.registerTool(
|
|
724
|
-
{
|
|
777
|
+
(ctx) => ({
|
|
725
778
|
name: "memory_get",
|
|
726
779
|
label: "Memory Get",
|
|
727
780
|
description: "Retrieve a specific memory by its ID from MemoryLake.",
|
|
@@ -729,10 +782,12 @@ const memoryPlugin = {
|
|
|
729
782
|
memoryId: Type.String({ description: "The memory ID to retrieve" }),
|
|
730
783
|
}),
|
|
731
784
|
async execute(_toolCallId, params) {
|
|
785
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
786
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
732
787
|
const { memoryId } = params as { memoryId: string };
|
|
733
788
|
|
|
734
789
|
try {
|
|
735
|
-
const memory = await
|
|
790
|
+
const memory = await effectiveProvider.get(memoryId);
|
|
736
791
|
|
|
737
792
|
return {
|
|
738
793
|
content: [
|
|
@@ -755,12 +810,12 @@ const memoryPlugin = {
|
|
|
755
810
|
};
|
|
756
811
|
}
|
|
757
812
|
},
|
|
758
|
-
},
|
|
813
|
+
}),
|
|
759
814
|
{ name: "memory_get" },
|
|
760
815
|
);
|
|
761
816
|
|
|
762
817
|
api.registerTool(
|
|
763
|
-
{
|
|
818
|
+
(ctx) => ({
|
|
764
819
|
name: "memory_list",
|
|
765
820
|
label: "Memory List",
|
|
766
821
|
description:
|
|
@@ -784,11 +839,13 @@ const memoryPlugin = {
|
|
|
784
839
|
),
|
|
785
840
|
}),
|
|
786
841
|
async execute(_toolCallId, params) {
|
|
842
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
843
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
787
844
|
const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
|
|
788
845
|
|
|
789
846
|
try {
|
|
790
|
-
const uid = userId ||
|
|
791
|
-
const memories = await
|
|
847
|
+
const uid = userId || effectiveCfg.userId;
|
|
848
|
+
const memories = await effectiveProvider.getAll({ user_id: uid });
|
|
792
849
|
|
|
793
850
|
if (!memories || memories.length === 0) {
|
|
794
851
|
return {
|
|
@@ -833,12 +890,12 @@ const memoryPlugin = {
|
|
|
833
890
|
};
|
|
834
891
|
}
|
|
835
892
|
},
|
|
836
|
-
},
|
|
893
|
+
}),
|
|
837
894
|
{ name: "memory_list" },
|
|
838
895
|
);
|
|
839
896
|
|
|
840
897
|
api.registerTool(
|
|
841
|
-
{
|
|
898
|
+
(ctx) => ({
|
|
842
899
|
name: "memory_forget",
|
|
843
900
|
label: "Memory Forget",
|
|
844
901
|
description:
|
|
@@ -847,10 +904,12 @@ const memoryPlugin = {
|
|
|
847
904
|
memoryId: Type.String({ description: "Memory ID to delete" }),
|
|
848
905
|
}),
|
|
849
906
|
async execute(_toolCallId, params) {
|
|
907
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
908
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
850
909
|
const { memoryId } = params as { memoryId: string };
|
|
851
910
|
|
|
852
911
|
try {
|
|
853
|
-
await
|
|
912
|
+
await effectiveProvider.delete(memoryId);
|
|
854
913
|
return {
|
|
855
914
|
content: [
|
|
856
915
|
{ type: "text", text: `Memory ${memoryId} forgotten.` },
|
|
@@ -869,12 +928,12 @@ const memoryPlugin = {
|
|
|
869
928
|
};
|
|
870
929
|
}
|
|
871
930
|
},
|
|
872
|
-
},
|
|
931
|
+
}),
|
|
873
932
|
{ name: "memory_forget" },
|
|
874
933
|
);
|
|
875
934
|
|
|
876
935
|
api.registerTool(
|
|
877
|
-
{
|
|
936
|
+
(ctx) => ({
|
|
878
937
|
name: "document_search",
|
|
879
938
|
label: "Document Search",
|
|
880
939
|
description:
|
|
@@ -889,12 +948,14 @@ const memoryPlugin = {
|
|
|
889
948
|
),
|
|
890
949
|
}),
|
|
891
950
|
async execute(_toolCallId, params) {
|
|
951
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
952
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
892
953
|
const { query, topN } = params as { query: string; topN?: number };
|
|
893
954
|
|
|
894
955
|
try {
|
|
895
|
-
const response = await
|
|
956
|
+
const response = await effectiveProvider.searchDocuments(
|
|
896
957
|
query,
|
|
897
|
-
topN ??
|
|
958
|
+
topN ?? effectiveCfg.topK,
|
|
898
959
|
);
|
|
899
960
|
|
|
900
961
|
if (!response.results || response.results.length === 0) {
|
|
@@ -929,12 +990,12 @@ const memoryPlugin = {
|
|
|
929
990
|
};
|
|
930
991
|
}
|
|
931
992
|
},
|
|
932
|
-
},
|
|
993
|
+
}),
|
|
933
994
|
{ name: "document_search" },
|
|
934
995
|
);
|
|
935
996
|
|
|
936
997
|
api.registerTool(
|
|
937
|
-
{
|
|
998
|
+
(ctx) => ({
|
|
938
999
|
name: "advanced_web_search",
|
|
939
1000
|
label: "Advanced Web Search",
|
|
940
1001
|
description:
|
|
@@ -986,6 +1047,8 @@ const memoryPlugin = {
|
|
|
986
1047
|
),
|
|
987
1048
|
}),
|
|
988
1049
|
async execute(_toolCallId, params) {
|
|
1050
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
1051
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
989
1052
|
const {
|
|
990
1053
|
query,
|
|
991
1054
|
domain: rawDomain,
|
|
@@ -1005,18 +1068,18 @@ const memoryPlugin = {
|
|
|
1005
1068
|
: normalizeWebSearchDomain(rawDomain);
|
|
1006
1069
|
|
|
1007
1070
|
try {
|
|
1008
|
-
const response = await
|
|
1071
|
+
const response = await effectiveProvider.searchWeb(query, {
|
|
1009
1072
|
domain,
|
|
1010
|
-
max_results: maxResults ??
|
|
1073
|
+
max_results: maxResults ?? effectiveCfg.topK,
|
|
1011
1074
|
start_date: startDate,
|
|
1012
1075
|
end_date: endDate,
|
|
1013
|
-
include_domains:
|
|
1014
|
-
exclude_domains:
|
|
1076
|
+
include_domains: effectiveCfg.webSearchIncludeDomains,
|
|
1077
|
+
exclude_domains: effectiveCfg.webSearchExcludeDomains,
|
|
1015
1078
|
user_location:
|
|
1016
|
-
|
|
1079
|
+
effectiveCfg.webSearchCountry || effectiveCfg.webSearchTimezone
|
|
1017
1080
|
? {
|
|
1018
|
-
country:
|
|
1019
|
-
timezone:
|
|
1081
|
+
country: effectiveCfg.webSearchCountry,
|
|
1082
|
+
timezone: effectiveCfg.webSearchTimezone,
|
|
1020
1083
|
}
|
|
1021
1084
|
: undefined,
|
|
1022
1085
|
});
|
|
@@ -1057,7 +1120,7 @@ const memoryPlugin = {
|
|
|
1057
1120
|
};
|
|
1058
1121
|
}
|
|
1059
1122
|
},
|
|
1060
|
-
},
|
|
1123
|
+
}),
|
|
1061
1124
|
{ optional: true },
|
|
1062
1125
|
);
|
|
1063
1126
|
|
|
@@ -1081,7 +1144,7 @@ const memoryPlugin = {
|
|
|
1081
1144
|
const limit = parseInt(opts.limit, 10);
|
|
1082
1145
|
const results = await provider.search(
|
|
1083
1146
|
query,
|
|
1084
|
-
buildSearchOptions(undefined, limit),
|
|
1147
|
+
buildSearchOptions(cfg, undefined, limit),
|
|
1085
1148
|
);
|
|
1086
1149
|
|
|
1087
1150
|
if (!results.length) {
|
|
@@ -1133,13 +1196,17 @@ const memoryPlugin = {
|
|
|
1133
1196
|
api.on("before_agent_start", async (event, ctx) => {
|
|
1134
1197
|
if (!event.prompt || event.prompt.length < 5) return;
|
|
1135
1198
|
|
|
1199
|
+
// Resolve per-workspace config override
|
|
1200
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
1201
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
1202
|
+
|
|
1136
1203
|
// Track session ID
|
|
1137
1204
|
const sessionId = (ctx as any)?.sessionKey ?? undefined;
|
|
1138
1205
|
if (sessionId) currentSessionId = sessionId;
|
|
1139
1206
|
|
|
1140
1207
|
const [memoryResult, docResult] = await Promise.allSettled([
|
|
1141
|
-
|
|
1142
|
-
|
|
1208
|
+
effectiveProvider.search(event.prompt, buildSearchOptions(effectiveCfg)),
|
|
1209
|
+
effectiveProvider.searchDocuments(event.prompt, effectiveCfg.topK),
|
|
1143
1210
|
]);
|
|
1144
1211
|
|
|
1145
1212
|
const contextParts: string[] = [];
|
|
@@ -1183,6 +1250,10 @@ const memoryPlugin = {
|
|
|
1183
1250
|
return;
|
|
1184
1251
|
}
|
|
1185
1252
|
|
|
1253
|
+
// Resolve per-workspace config override
|
|
1254
|
+
const effectiveCfg = resolveConfig(ctx);
|
|
1255
|
+
const effectiveProvider = getProvider(effectiveCfg);
|
|
1256
|
+
|
|
1186
1257
|
// Track session ID
|
|
1187
1258
|
const sessionId = (ctx as any)?.sessionKey ?? undefined;
|
|
1188
1259
|
if (sessionId) currentSessionId = sessionId;
|
|
@@ -1240,8 +1311,8 @@ const memoryPlugin = {
|
|
|
1240
1311
|
|
|
1241
1312
|
if (formattedMessages.length === 0) return;
|
|
1242
1313
|
|
|
1243
|
-
const addOpts = buildAddOptions(undefined, currentSessionId);
|
|
1244
|
-
const result = await
|
|
1314
|
+
const addOpts = buildAddOptions(effectiveCfg, undefined, currentSessionId);
|
|
1315
|
+
const result = await effectiveProvider.add(
|
|
1245
1316
|
formattedMessages,
|
|
1246
1317
|
addOpts,
|
|
1247
1318
|
);
|
package/package.json
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-memorylake-config
|
|
3
|
+
description: Use when the user asks to configure agent-specific memorylake properties (e.g. projectId) for the current agent. Writes to the agent-specific config file which overrides the global config.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent MemoryLake Config
|
|
7
|
+
|
|
8
|
+
Configure agent-specific memorylake properties for the current agent. The config file is located at `{workspace}/.memorylake/config.json`, where `{workspace}` is the agent's workspace directory. This config overrides corresponding properties from the global config.
|
|
9
|
+
|
|
10
|
+
## Step 1 — Confirm projectId
|
|
11
|
+
|
|
12
|
+
Ask the user for the `projectId` to configure.
|
|
13
|
+
|
|
14
|
+
If the user has already provided the `projectId` in their message, skip the question and proceed directly.
|
|
15
|
+
|
|
16
|
+
## Step 2 — Write Config
|
|
17
|
+
|
|
18
|
+
1. Ensure the `.memorylake/` directory exists inside the agent's workspace directory:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd {workspace}
|
|
22
|
+
mkdir -p .memorylake
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. If `.memorylake/config.json` already exists, read it first and merge the new properties into the existing config. Do NOT overwrite properties the user did not mention.
|
|
26
|
+
|
|
27
|
+
3. If `.memorylake/config.json` does not exist, create it with the provided properties.
|
|
28
|
+
|
|
29
|
+
4. Write the config file. Example format:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"projectId": "xxx"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Step 3 — Confirm Result
|
|
38
|
+
|
|
39
|
+
Read the written `.memorylake/config.json` and confirm to the user that the configuration is complete.
|
|
40
|
+
|
|
41
|
+
## Common Mistakes
|
|
42
|
+
|
|
43
|
+
- Do NOT overwrite existing properties that the user did not mention — always merge
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memorylake-api
|
|
3
|
+
description: Use when the user asks about MemoryLake features, capabilities, or wants to perform a MemoryLake action but no specific tool or skill matches. This is the catch-all for any MemoryLake-related request -- discovers available APIs from the remote OpenAPI spec and calls them directly.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MemoryLake API
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Directly call MemoryLake's REST APIs by discovering endpoints from the live OpenAPI spec. This skill covers any MemoryLake capability not already handled by a dedicated tool or skill — project management, document management, file uploads, memory trace, statistics, and more.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- User asks about MemoryLake capabilities or features, and no existing tool or skill covers the request
|
|
15
|
+
- User wants to manage MemoryLake projects (create, update, delete, list, view stats)
|
|
16
|
+
- User wants to manage documents (upload files, add to project, list, delete)
|
|
17
|
+
- User wants to view the change history (trace) of a memory
|
|
18
|
+
- User wants to call any MemoryLake API endpoint directly
|
|
19
|
+
|
|
20
|
+
## Step 1 — Read MemoryLake Config
|
|
21
|
+
|
|
22
|
+
Read `~/.openclaw/openclaw.json` and extract the plugin config:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cat ~/.openclaw/openclaw.json | jq '.plugins.entries["memorylake-openclaw"].config'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Extract these values:
|
|
29
|
+
|
|
30
|
+
| Variable | Description | Default |
|
|
31
|
+
|----------|-------------|---------|
|
|
32
|
+
| `host` | API host | `https://app.memorylake.ai` |
|
|
33
|
+
| `apiKey` | API key for authentication | (required) |
|
|
34
|
+
| `projectId` | MemoryLake project ID | (required) |
|
|
35
|
+
|
|
36
|
+
If `apiKey` or `projectId` is missing, stop and inform the user.
|
|
37
|
+
|
|
38
|
+
Auth header for all requests:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Authorization: Bearer {apiKey}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Step 2 — Identify the Right API Endpoint
|
|
45
|
+
|
|
46
|
+
### Option A: Use the Quick Reference Table
|
|
47
|
+
|
|
48
|
+
If the user's intent clearly maps to one of the endpoints below, skip to Step 3.
|
|
49
|
+
|
|
50
|
+
#### Projects
|
|
51
|
+
|
|
52
|
+
| Method | Path | Description |
|
|
53
|
+
|--------|------|-------------|
|
|
54
|
+
| `GET` | `/api/v1/projects` | List projects (paginated, filterable by name) |
|
|
55
|
+
| `POST` | `/api/v1/projects` | Create a new project |
|
|
56
|
+
| `GET` | `/api/v1/projects/{id}` | Get project details (includes stats) |
|
|
57
|
+
| `PUT` | `/api/v1/projects/{id}` | Update project name/description |
|
|
58
|
+
| `DELETE` | `/api/v1/projects/{id}` | Delete a project |
|
|
59
|
+
|
|
60
|
+
#### Memories (V2)
|
|
61
|
+
|
|
62
|
+
| Method | Path | Description |
|
|
63
|
+
|--------|------|-------------|
|
|
64
|
+
| `POST` | `/api/v2/projects/{id}/memories` | Add memory from conversation |
|
|
65
|
+
| `GET` | `/api/v2/projects/{id}/memories` | List memories (paginated, filter by `user_id`/`keyword`) |
|
|
66
|
+
| `POST` | `/api/v2/projects/{id}/memories/search` | Search memories by natural language |
|
|
67
|
+
| `GET` | `/api/v2/projects/{id}/memories/{memoryId}` | Get a single memory |
|
|
68
|
+
| `DELETE` | `/api/v2/projects/{id}/memories/{memoryId}` | Delete a memory |
|
|
69
|
+
| `GET` | `/api/v2/projects/{id}/memories/{memoryId}/trace` | Get memory change history |
|
|
70
|
+
|
|
71
|
+
#### Documents
|
|
72
|
+
|
|
73
|
+
| Method | Path | Description |
|
|
74
|
+
|--------|------|-------------|
|
|
75
|
+
| `GET` | `/api/v1/projects/{id}/documents` | List documents in project (paginated) |
|
|
76
|
+
| `GET` | `/api/v1/projects/{id}/documents/{documentId}` | Get document details |
|
|
77
|
+
| `DELETE` | `/api/v1/projects/{id}/documents` | Batch delete documents |
|
|
78
|
+
| `POST` | `/api/v1/projects/{id}/documents/search` | Semantic search over documents |
|
|
79
|
+
|
|
80
|
+
### Option B: Fetch the Live OpenAPI Spec
|
|
81
|
+
|
|
82
|
+
If the user's request is ambiguous or might involve a new/undocumented endpoint, fetch the spec:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl -s "{host}/openapi/memorylake/api-docs/open-api" | jq '.paths | keys'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To inspect a specific endpoint's schema:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
curl -s "{host}/openapi/memorylake/api-docs/open-api" | jq '.paths["/api/v1/projects/{id}"]'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To inspect request/response schemas:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
curl -s "{host}/openapi/memorylake/api-docs/open-api" | jq '.components.schemas["ProjectCreateRequest"]'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Step 3 — Construct and Execute the API Call
|
|
101
|
+
|
|
102
|
+
Build a `curl` command using:
|
|
103
|
+
|
|
104
|
+
- **Base URL**: `{host}/openapi/memorylake` (the server base path from the OpenAPI spec)
|
|
105
|
+
- **Full URL**: `{host}/openapi/memorylake{path}` (e.g., `{host}/openapi/memorylake/api/v1/projects`)
|
|
106
|
+
- **Path params**: replace `{id}` with `{projectId}` from config (for project-scoped endpoints)
|
|
107
|
+
- **Auth header**: `Authorization: Bearer {apiKey}`
|
|
108
|
+
- **Content-Type**: `application/json` (for POST/PUT requests with a body)
|
|
109
|
+
|
|
110
|
+
### Example: List Projects
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -s -X GET "{host}/openapi/memorylake/api/v1/projects?page=1&size=20" \
|
|
114
|
+
-H "Authorization: Bearer {apiKey}" | jq
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Example: Create a Project
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
curl -s -X POST "{host}/openapi/memorylake/api/v1/projects" \
|
|
121
|
+
-H "Authorization: Bearer {apiKey}" \
|
|
122
|
+
-H "Content-Type: application/json" \
|
|
123
|
+
-d '{
|
|
124
|
+
"name": "My New Project",
|
|
125
|
+
"description": "Project description"
|
|
126
|
+
}' | jq
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Example: Get Project Details (with Stats)
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
curl -s -X GET "{host}/openapi/memorylake/api/v1/projects/{projectId}" \
|
|
133
|
+
-H "Authorization: Bearer {apiKey}" | jq
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Example: Get Memory Trace
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
curl -s -X GET "{host}/openapi/memorylake/api/v2/projects/{projectId}/memories/{memoryId}/trace" \
|
|
140
|
+
-H "Authorization: Bearer {apiKey}" | jq
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Example: Search Documents
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
curl -s -X POST "{host}/openapi/memorylake/api/v1/projects/{projectId}/documents/search" \
|
|
147
|
+
-H "Authorization: Bearer {apiKey}" \
|
|
148
|
+
-H "Content-Type: application/json" \
|
|
149
|
+
-d '{
|
|
150
|
+
"query": "quarterly sales figures",
|
|
151
|
+
"top_n": 10
|
|
152
|
+
}' | jq
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Step 4 — Present Results
|
|
156
|
+
|
|
157
|
+
Parse the JSON response and present it to the user in a readable format:
|
|
158
|
+
|
|
159
|
+
- Check `success` field — if `false`, report `message` and `error_code`
|
|
160
|
+
- For list/search responses, format the `data.items` or `data.results` array as a table or structured list
|
|
161
|
+
- For single-item responses, display key fields clearly
|
|
162
|
+
- For paginated responses, report `page`, `total`, `total_pages` so the user knows if there are more results
|
|
163
|
+
|
|
164
|
+
## Error Handling
|
|
165
|
+
|
|
166
|
+
All responses follow the same wrapper format:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"success": true|false,
|
|
171
|
+
"message": "Human-readable message",
|
|
172
|
+
"data": { ... },
|
|
173
|
+
"error_code": "VALIDATION_ERROR"
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| HTTP Status | Meaning | Action |
|
|
178
|
+
|-------------|---------|--------|
|
|
179
|
+
| 200 | Success | Parse `data` field |
|
|
180
|
+
| 400 | Invalid request | Check `message` for validation details |
|
|
181
|
+
| 404 | Not found | Verify project ID / memory ID / document ID |
|
|
182
|
+
| 401/403 | Auth failure | Verify `apiKey` is correct and not expired |
|
|
183
|
+
|
|
184
|
+
## Common Mistakes
|
|
185
|
+
|
|
186
|
+
- **Wrong base URL**: The full URL must include `/openapi/memorylake` before the API path. E.g., `/openapi/memorylake/api/v1/projects`, NOT just `/api/v1/projects`
|
|
187
|
+
- **Missing auth header**: Every request requires `Authorization: Bearer {apiKey}`
|
|
188
|
+
- **Hardcoded project ID**: Always read `projectId` from `~/.openclaw/openclaw.json` config, not from user input (unless the user explicitly wants a different project)
|
|
189
|
+
- **Pagination**: List endpoints default to `page=1, size=20`. Pass `page` and `size` query params if the user needs more results
|
|
190
|
+
|
|
191
|
+
## Quick Reference
|
|
192
|
+
|
|
193
|
+
| Item | Value |
|
|
194
|
+
|------|-------|
|
|
195
|
+
| OpenClaw config | `~/.openclaw/openclaw.json` |
|
|
196
|
+
| Plugin config key | `plugins.entries["memorylake-openclaw"].config` |
|
|
197
|
+
| Server base path | `/openapi/memorylake` |
|
|
198
|
+
| OpenAPI spec URL | `{host}/openapi/memorylake/api-docs/open-api` |
|
|
199
|
+
| Auth header | `Authorization: Bearer {apiKey}` |
|
|
200
|
+
| Default host | `https://app.memorylake.ai` |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memorylake-upload
|
|
3
|
+
description: Use when the user wants to upload files, documents, PDFs, or other data files to MemoryLake and associate them with a project.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# MemoryLake File Upload
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Upload local files to MemoryLake using the multipart upload API, then associate them with a project.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- User wants to upload a file (PDF, DOCX, image, etc.) to MemoryLake
|
|
15
|
+
- User wants to add a local document to a MemoryLake project
|
|
16
|
+
|
|
17
|
+
## Step 1 -- Read MemoryLake Config
|
|
18
|
+
|
|
19
|
+
Read `~/.openclaw/openclaw.json` and extract the plugin config:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cat ~/.openclaw/openclaw.json | jq '.plugins.entries["memorylake-openclaw"].config'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Extract these values:
|
|
26
|
+
|
|
27
|
+
| Variable | Description | Default |
|
|
28
|
+
|----------|-------------|---------|
|
|
29
|
+
| `host` | API host | `https://app.memorylake.ai` |
|
|
30
|
+
| `apiKey` | API key for authentication | (required) |
|
|
31
|
+
| `projectId` | MemoryLake project ID | (required) |
|
|
32
|
+
|
|
33
|
+
If `apiKey` or `projectId` is missing, stop and inform the user.
|
|
34
|
+
|
|
35
|
+
## Step 2 -- Run the Upload Script
|
|
36
|
+
|
|
37
|
+
The upload script is at `scripts/upload.mjs` relative to **this skill's SKILL.md**.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
node {path-to-this-skill}/scripts/upload.mjs \
|
|
41
|
+
--host {host} \
|
|
42
|
+
--api-key {apiKey} \
|
|
43
|
+
--project-id {projectId} \
|
|
44
|
+
--file-name {fileName} \
|
|
45
|
+
/path/to/file
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`--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.
|
|
49
|
+
|
|
50
|
+
## Step 3 -- Handle Output
|
|
51
|
+
|
|
52
|
+
The script prints progress for each step (create upload, upload parts, complete, add to project).
|
|
53
|
+
|
|
54
|
+
- **Success**: Report the document ID and file name to the user
|
|
55
|
+
- **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
|
|
56
|
+
|
|
57
|
+
## Common Mistakes
|
|
58
|
+
|
|
59
|
+
- **Skipping Step 1**: Directly hardcoding host/apiKey/projectId instead of reading from `~/.openclaw/openclaw.json`
|
|
60
|
+
- **Relative file paths**: Always resolve the user's file path to an absolute path before passing to the script
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MemoryLake File Upload Script
|
|
5
|
+
*
|
|
6
|
+
* Uploads a local file to MemoryLake using multipart upload,
|
|
7
|
+
* then associates it with a project.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node upload.mjs --host <url> --api-key <key> --project-id <id> <file_path>
|
|
11
|
+
*
|
|
12
|
+
* Parameters:
|
|
13
|
+
* host - Base URL (e.g., http://10.71.10.71:3002)
|
|
14
|
+
* apiKey - API key for authentication
|
|
15
|
+
* projectId - Project ID to associate the document with (required)
|
|
16
|
+
* filePath - Path to the file to upload
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import https from 'https';
|
|
22
|
+
import http from 'http';
|
|
23
|
+
|
|
24
|
+
// API base path
|
|
25
|
+
const API_BASE = '/openapi/memorylake';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Make an HTTP request
|
|
29
|
+
*/
|
|
30
|
+
function request(method, urlStr, body = null, headers = {}) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const url = new URL(urlStr);
|
|
33
|
+
const isHttps = url.protocol === 'https:';
|
|
34
|
+
const lib = isHttps ? https : http;
|
|
35
|
+
|
|
36
|
+
const options = {
|
|
37
|
+
hostname: url.hostname,
|
|
38
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
39
|
+
path: url.pathname + url.search,
|
|
40
|
+
method,
|
|
41
|
+
headers: {
|
|
42
|
+
...headers,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (body && typeof body === 'object' && !(body instanceof Buffer)) {
|
|
47
|
+
options.headers['Content-Type'] = 'application/json';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const req = lib.request(options, (res) => {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
53
|
+
res.on('end', () => {
|
|
54
|
+
const buffer = Buffer.concat(chunks);
|
|
55
|
+
const text = buffer.toString('utf8');
|
|
56
|
+
resolve({
|
|
57
|
+
status: res.statusCode,
|
|
58
|
+
headers: res.headers,
|
|
59
|
+
body: text,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
req.on('error', reject);
|
|
65
|
+
|
|
66
|
+
if (body) {
|
|
67
|
+
if (body instanceof Buffer) {
|
|
68
|
+
req.write(body);
|
|
69
|
+
} else if (typeof body === 'object') {
|
|
70
|
+
req.write(JSON.stringify(body));
|
|
71
|
+
} else {
|
|
72
|
+
req.write(body);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create multipart upload session
|
|
82
|
+
*/
|
|
83
|
+
async function createMultipartUpload(host, apiKey, fileSize) {
|
|
84
|
+
const url = `${host}${API_BASE}/api/v1/upload/create-multipart`;
|
|
85
|
+
const response = await request('POST', url, { file_size: fileSize }, {
|
|
86
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const data = JSON.parse(response.body);
|
|
91
|
+
if (!data.success) {
|
|
92
|
+
throw new Error(`Create multipart failed: ${data.message || JSON.stringify(data)}`);
|
|
93
|
+
}
|
|
94
|
+
return data.data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Upload a single part to pre-signed URL
|
|
99
|
+
*/
|
|
100
|
+
async function uploadPart(uploadUrl, buffer, partNumber, totalParts) {
|
|
101
|
+
process.stdout.write(` Uploading part ${partNumber + 1}/${totalParts}...`);
|
|
102
|
+
|
|
103
|
+
const response = await request('PUT', uploadUrl, buffer, {
|
|
104
|
+
'Content-Length': buffer.length.toString(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (response.status < 200 || response.status >= 300) {
|
|
108
|
+
throw new Error(`Part upload failed with status ${response.status}: ${response.body}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get ETag from response headers (may be quoted)
|
|
112
|
+
let etag = response.headers['etag'] || response.headers['ETag'];
|
|
113
|
+
if (etag) {
|
|
114
|
+
etag = etag.replace(/"/g, '');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(` done (ETag: ${etag || 'none'})`);
|
|
118
|
+
return etag;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Complete multipart upload
|
|
123
|
+
*/
|
|
124
|
+
async function completeMultipartUpload(host, apiKey, uploadId, objectKey, partEtags) {
|
|
125
|
+
const url = `${host}${API_BASE}/api/v1/upload/complete-multipart`;
|
|
126
|
+
const response = await request('POST', url, {
|
|
127
|
+
upload_id: uploadId,
|
|
128
|
+
object_key: objectKey,
|
|
129
|
+
part_etags: partEtags,
|
|
130
|
+
}, {
|
|
131
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const data = JSON.parse(response.body);
|
|
136
|
+
if (!data.success) {
|
|
137
|
+
throw new Error(`Complete multipart failed: ${data.message || JSON.stringify(data)}`);
|
|
138
|
+
}
|
|
139
|
+
return data;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Add document to project
|
|
144
|
+
*/
|
|
145
|
+
async function quickAddDocument(host, apiKey, projectId, objectKey, fileName) {
|
|
146
|
+
const url = `${host}${API_BASE}/api/v1/projects/${projectId}/documents/quick-add`;
|
|
147
|
+
const response = await request('POST', url, {
|
|
148
|
+
object_key: objectKey,
|
|
149
|
+
file_name: fileName,
|
|
150
|
+
}, {
|
|
151
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const data = JSON.parse(response.body);
|
|
156
|
+
if (!data.success) {
|
|
157
|
+
throw new Error(`Quick add document failed: ${data.message || JSON.stringify(data)}`);
|
|
158
|
+
}
|
|
159
|
+
return data.data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Format file size for display
|
|
164
|
+
*/
|
|
165
|
+
function formatSize(bytes) {
|
|
166
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
167
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
168
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
169
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Main upload function
|
|
174
|
+
*/
|
|
175
|
+
export async function upload({ host, apiKey, projectId, filePath, fileName }) {
|
|
176
|
+
if (!host) throw new Error('host is required');
|
|
177
|
+
if (!apiKey) throw new Error('apiKey is required');
|
|
178
|
+
if (!projectId) throw new Error('projectId is required');
|
|
179
|
+
if (!filePath) throw new Error('filePath is required');
|
|
180
|
+
|
|
181
|
+
if (!fs.existsSync(filePath)) {
|
|
182
|
+
throw new Error(`File not found: ${filePath}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stats = fs.statSync(filePath);
|
|
186
|
+
const fileSize = stats.size;
|
|
187
|
+
if (!fileName) fileName = path.basename(filePath);
|
|
188
|
+
|
|
189
|
+
console.log(`\nUploading: ${fileName} (${formatSize(fileSize)})`);
|
|
190
|
+
|
|
191
|
+
// Step 1: Create multipart upload
|
|
192
|
+
console.log('Creating multipart upload...');
|
|
193
|
+
const uploadInfo = await createMultipartUpload(host, apiKey, fileSize);
|
|
194
|
+
const { upload_id, object_key, part_items } = uploadInfo;
|
|
195
|
+
console.log(` Upload ID: ${upload_id}`);
|
|
196
|
+
console.log(` Object Key: ${object_key}`);
|
|
197
|
+
console.log(` Parts: ${part_items.length}`);
|
|
198
|
+
|
|
199
|
+
// Step 2: Upload each part (stream each chunk to avoid loading entire file into memory)
|
|
200
|
+
console.log('\nUploading parts:');
|
|
201
|
+
const fd = fs.openSync(filePath, 'r');
|
|
202
|
+
const partEtags = [];
|
|
203
|
+
|
|
204
|
+
let offset = 0;
|
|
205
|
+
for (const part of part_items) {
|
|
206
|
+
const partBuffer = Buffer.alloc(part.size);
|
|
207
|
+
fs.readSync(fd, partBuffer, 0, part.size, offset);
|
|
208
|
+
const etag = await uploadPart(part.upload_url, partBuffer, part.number, part_items.length);
|
|
209
|
+
partEtags.push({
|
|
210
|
+
number: part.number,
|
|
211
|
+
etag: etag,
|
|
212
|
+
});
|
|
213
|
+
offset += part.size;
|
|
214
|
+
}
|
|
215
|
+
fs.closeSync(fd);
|
|
216
|
+
|
|
217
|
+
// Step 3: Complete multipart upload
|
|
218
|
+
console.log('\nCompleting multipart upload...');
|
|
219
|
+
await completeMultipartUpload(host, apiKey, upload_id, object_key, partEtags);
|
|
220
|
+
console.log(' Upload completed');
|
|
221
|
+
|
|
222
|
+
// Step 4: Add to project
|
|
223
|
+
console.log(`\nAdding to project: ${projectId}`);
|
|
224
|
+
const doc = await quickAddDocument(host, apiKey, projectId, object_key, fileName);
|
|
225
|
+
console.log(' Document added to project');
|
|
226
|
+
console.log(` Document ID: ${doc.document_id}`);
|
|
227
|
+
console.log(` File name: ${doc.file_name}`);
|
|
228
|
+
return doc;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// CLI entry point
|
|
232
|
+
async function main() {
|
|
233
|
+
const args = process.argv.slice(2);
|
|
234
|
+
|
|
235
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
236
|
+
console.log(`
|
|
237
|
+
MemoryLake File Upload
|
|
238
|
+
|
|
239
|
+
Usage:
|
|
240
|
+
node upload.mjs --host <url> --api-key <key> --project-id <id> <file_path>
|
|
241
|
+
|
|
242
|
+
Arguments:
|
|
243
|
+
--host Base URL (e.g., http://10.71.10.71:3002)
|
|
244
|
+
--api-key API key for authentication
|
|
245
|
+
--project-id Project ID to associate the document with (required)
|
|
246
|
+
--file-name Custom file name (default: basename of file_path)
|
|
247
|
+
file_path Path to the file to upload
|
|
248
|
+
|
|
249
|
+
Examples:
|
|
250
|
+
node upload.mjs --host http://10.71.10.71:3002 --api-key sk-xxx --project-id proj-abc123 document.pdf
|
|
251
|
+
`);
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let host, apiKey, projectId, filePath, fileName;
|
|
256
|
+
for (let i = 0; i < args.length; i++) {
|
|
257
|
+
if (args[i] === '--host') host = args[++i];
|
|
258
|
+
else if (args[i] === '--api-key') apiKey = args[++i];
|
|
259
|
+
else if (args[i] === '--project-id') projectId = args[++i];
|
|
260
|
+
else if (args[i] === '--file-name') fileName = args[++i];
|
|
261
|
+
else if (!args[i].startsWith('-')) filePath = args[i];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await upload({ host, apiKey, projectId, filePath, fileName });
|
|
266
|
+
console.log('\nDone!\n');
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error(`\nError: ${err.message}\n`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main();
|