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 || cfg.userId,
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 || cfg.userId,
558
- top_k: limit ?? cfg.topK,
559
- threshold: cfg.searchThreshold,
560
- rerank: cfg.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 provider.search(
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 provider.add(
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 provider.get(memoryId);
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 || cfg.userId;
791
- const memories = await provider.getAll({ user_id: uid });
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 provider.delete(memoryId);
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 provider.searchDocuments(
956
+ const response = await effectiveProvider.searchDocuments(
896
957
  query,
897
- topN ?? cfg.topK,
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 provider.searchWeb(query, {
1071
+ const response = await effectiveProvider.searchWeb(query, {
1009
1072
  domain,
1010
- max_results: maxResults ?? cfg.topK,
1073
+ max_results: maxResults ?? effectiveCfg.topK,
1011
1074
  start_date: startDate,
1012
1075
  end_date: endDate,
1013
- include_domains: cfg.webSearchIncludeDomains,
1014
- exclude_domains: cfg.webSearchExcludeDomains,
1076
+ include_domains: effectiveCfg.webSearchIncludeDomains,
1077
+ exclude_domains: effectiveCfg.webSearchExcludeDomains,
1015
1078
  user_location:
1016
- cfg.webSearchCountry || cfg.webSearchTimezone
1079
+ effectiveCfg.webSearchCountry || effectiveCfg.webSearchTimezone
1017
1080
  ? {
1018
- country: cfg.webSearchCountry,
1019
- timezone: cfg.webSearchTimezone,
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
- provider.search(event.prompt, buildSearchOptions()),
1142
- provider.searchDocuments(event.prompt, cfg.topK),
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 provider.add(
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",
@@ -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();