memorylake-openclaw 0.0.5 → 0.0.7

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/README.md CHANGED
@@ -37,7 +37,7 @@ Get an API key from [app.memorylake.ai](https://app.memorylake.ai), then add to
37
37
 
38
38
  ## Agent tools
39
39
 
40
- The agent gets six tools it can call during conversations:
40
+ The agent gets seven tools it can call during conversations:
41
41
 
42
42
  | Tool | Description |
43
43
  |------|-------------|
@@ -47,6 +47,7 @@ The agent gets six tools it can call during conversations:
47
47
  | `memory_get` | Retrieve a memory by ID |
48
48
  | `memory_forget` | Delete a memory by ID |
49
49
  | `document_search` | Search project documents for relevant paragraphs, tables, and figures |
50
+ | `advanced_web_search` | Optional tool for web search with plugin-level domain and locale constraints |
50
51
 
51
52
  ## CLI
52
53
 
@@ -70,6 +71,12 @@ openclaw memorylake stats
70
71
  | `topK` | `number` | `5` | Max memories per recall |
71
72
  | `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
72
73
  | `rerank` | `boolean` | `true` | Rerank search results |
74
+ | `webSearchIncludeDomains` | `string[]` | — | Optional allowlist for `advanced_web_search` results |
75
+ | `webSearchExcludeDomains` | `string[]` | — | Optional denylist for `advanced_web_search` results |
76
+ | `webSearchCountry` | `string` | — | Optional ISO country code for localizing `advanced_web_search` |
77
+ | `webSearchTimezone` | `string` | — | Optional IANA timezone for localizing `advanced_web_search` |
78
+
79
+ `advanced_web_search` is registered as an optional OpenClaw tool, so agents must explicitly allow it before they can call it.
73
80
 
74
81
  ## License
75
82
 
package/docs/openclaw.mdx CHANGED
@@ -13,7 +13,7 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents
13
13
  The plugin provides:
14
14
  1. **Auto-Recall** — Before the agent responds, memories and relevant document excerpts matching the current message are injected into context
15
15
  2. **Auto-Capture** — After the agent responds, the exchange is sent to MemoryLake which decides what's worth keeping
16
- 3. **Agent Tools** — Six tools for memory and document operations during conversations
16
+ 3. **Agent Tools** — Seven tools for memory, document, and optional web search operations during conversations
17
17
 
18
18
  Both auto-recall and auto-capture run silently with no manual configuration required.
19
19
 
@@ -42,7 +42,7 @@ Add to your `openclaw.json`:
42
42
 
43
43
  ## Agent Tools
44
44
 
45
- The agent gets six tools it can call during conversations:
45
+ The agent gets seven tools it can call during conversations:
46
46
 
47
47
  | Tool | Description |
48
48
  |------|-------------|
@@ -52,6 +52,7 @@ The agent gets six tools it can call during conversations:
52
52
  | `memory_get` | Retrieve a memory by ID |
53
53
  | `memory_forget` | Delete a memory by ID |
54
54
  | `document_search` | Search project documents for relevant paragraphs, tables, and figures |
55
+ | `advanced_web_search` | Optional web search tool backed by the unified search API with plugin-level domain and locale constraints |
55
56
 
56
57
  ## CLI Commands
57
58
 
@@ -75,13 +76,19 @@ openclaw memorylake stats
75
76
  | `topK` | `number` | `5` | Max memories per recall |
76
77
  | `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
77
78
  | `rerank` | `boolean` | `true` | Rerank search results for better relevance |
79
+ | `webSearchIncludeDomains` | `string[]` | — | Optional allowlist for `advanced_web_search` results |
80
+ | `webSearchExcludeDomains` | `string[]` | — | Optional denylist for `advanced_web_search` results |
81
+ | `webSearchCountry` | `string` | — | Optional ISO country code for localizing `advanced_web_search` |
82
+ | `webSearchTimezone` | `string` | — | Optional IANA timezone for localizing `advanced_web_search` |
83
+
84
+ <Note>`advanced_web_search` is registered as an optional OpenClaw tool, so it must be explicitly allowed before an agent can call it.</Note>
78
85
 
79
86
  ## Key Features
80
87
 
81
88
  1. **Zero Configuration** — Auto-recall and auto-capture work out of the box with no prompting required
82
89
  2. **Async Processing** — Memory extraction runs asynchronously via MemoryLake's API
83
90
  3. **Session Tracking** — Conversations are tagged with `chat_session_id` for traceability
84
- 4. **Rich Tool Suite** — Six agent tools for memory and document operations when needed
91
+ 4. **Rich Tool Suite** — Seven agent tools for memory, document, and optional web search operations when needed
85
92
 
86
93
  ## Conclusion
87
94
 
package/index.ts CHANGED
@@ -4,12 +4,14 @@
4
4
  * Long-term memory via MemoryLake platform.
5
5
  *
6
6
  * Features:
7
- * - 6 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search
7
+ * - 7 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search, advanced_web_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
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";
@@ -29,6 +31,10 @@ type MemoryLakeConfig = {
29
31
  searchThreshold: number;
30
32
  topK: number;
31
33
  rerank: boolean;
34
+ webSearchIncludeDomains?: string[];
35
+ webSearchExcludeDomains?: string[];
36
+ webSearchCountry?: string;
37
+ webSearchTimezone?: string;
32
38
  };
33
39
 
34
40
  // V2 API option types
@@ -99,6 +105,68 @@ interface DocumentSearchResponse {
99
105
  results: DocumentSearchResult[];
100
106
  }
101
107
 
108
+ /**
109
+ * Allowed values for web search domain (aligned with zootopia unified_search Domain).
110
+ * Declared as enum in schema; at runtime accept string and fall back to "auto" if invalid.
111
+ */
112
+ const WebSearchDomainValues = [
113
+ "web",
114
+ "academic",
115
+ "news",
116
+ "people",
117
+ "company",
118
+ "financial",
119
+ "markets",
120
+ "code",
121
+ "legal",
122
+ "government",
123
+ "poi",
124
+ "auto",
125
+ ] as const;
126
+ type WebSearchDomain = (typeof WebSearchDomainValues)[number];
127
+
128
+ const WEB_SEARCH_DOMAIN_SET = new Set<string>(WebSearchDomainValues);
129
+
130
+ /** Normalize domain: accept string at runtime; if not a valid enum value, return "auto". */
131
+ function normalizeWebSearchDomain(value: unknown): WebSearchDomain {
132
+ if (value == null) return "auto";
133
+ const s = typeof value === "string" ? value.toLowerCase().trim() : "";
134
+ return (WEB_SEARCH_DOMAIN_SET.has(s) ? s : "auto") as WebSearchDomain;
135
+ }
136
+
137
+ interface WebSearchUserLocation {
138
+ country?: string;
139
+ timezone?: string;
140
+ }
141
+
142
+ interface WebSearchOptions {
143
+ /** Declared as enum in schema; at runtime accept string, normalized with fallback to "auto". */
144
+ domain?: WebSearchDomain | string;
145
+ max_results?: number;
146
+ start_date?: string;
147
+ end_date?: string;
148
+ include_domains?: string[];
149
+ exclude_domains?: string[];
150
+ user_location?: WebSearchUserLocation;
151
+ }
152
+
153
+ interface WebSearchResult {
154
+ url?: string;
155
+ title?: string;
156
+ summary?: string;
157
+ content?: string;
158
+ source?: string;
159
+ published_date?: string;
160
+ author?: string;
161
+ score?: number;
162
+ highlights?: string[];
163
+ }
164
+
165
+ interface WebSearchResponse {
166
+ results: WebSearchResult[];
167
+ total_results: number;
168
+ }
169
+
102
170
  // ============================================================================
103
171
  // Unified Provider Interface
104
172
  // ============================================================================
@@ -113,6 +181,7 @@ interface MemoryLakeProvider {
113
181
  getAll(options: ListOptions): Promise<MemoryItem[]>;
114
182
  delete(memoryId: string): Promise<void>;
115
183
  searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse>;
184
+ searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse>;
116
185
  }
117
186
 
118
187
  // ============================================================================
@@ -130,10 +199,12 @@ class PlatformProvider implements MemoryLakeProvider {
130
199
  private readonly http: ReturnType<typeof got.extend>;
131
200
  private readonly basePath: string;
132
201
  private readonly docSearchPath: string;
202
+ private readonly webSearchPath: string;
133
203
 
134
204
  constructor(host: string, apiKey: string, projectId: string) {
135
205
  this.basePath = `openapi/memorylake/api/v2/projects/${projectId}/memories`;
136
206
  this.docSearchPath = `openapi/memorylake/api/v1/projects/${projectId}/documents/search`;
207
+ this.webSearchPath = "openapi/memorylake/api/v1/search";
137
208
  this.http = got.extend({
138
209
  prefixUrl: host,
139
210
  headers: {
@@ -220,6 +291,25 @@ class PlatformProvider implements MemoryLakeProvider {
220
291
  results: Array.isArray(data?.results) ? data.results : [],
221
292
  };
222
293
  }
294
+
295
+ async searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse> {
296
+ const domain = options.domain != null ? normalizeWebSearchDomain(options.domain) : "web";
297
+ const body: Record<string, unknown> = {
298
+ query,
299
+ domain,
300
+ };
301
+ if (options.max_results != null) body.max_results = options.max_results;
302
+ if (options.start_date) body.start_date = options.start_date;
303
+ if (options.end_date) body.end_date = options.end_date;
304
+ if (options.include_domains?.length) body.include_domains = options.include_domains;
305
+ if (options.exclude_domains?.length) body.exclude_domains = options.exclude_domains;
306
+ if (options.user_location) body.user_location = options.user_location;
307
+
308
+ const resp = await this.http
309
+ .post(this.webSearchPath, { json: body })
310
+ .json<WebSearchResponse>();
311
+ return normalizeWebSearchResponse(resp);
312
+ }
223
313
  }
224
314
 
225
315
  // ============================================================================
@@ -254,6 +344,13 @@ function normalizeAddResult(raw: any): AddResult {
254
344
  };
255
345
  }
256
346
 
347
+ function normalizeWebSearchResponse(raw: any): WebSearchResponse {
348
+ return {
349
+ results: Array.isArray(raw?.results) ? raw.results : [],
350
+ total_results: typeof raw?.total_results === "number" ? raw.total_results : 0,
351
+ };
352
+ }
353
+
257
354
  // ============================================================================
258
355
  // Document Context Builder
259
356
  // ============================================================================
@@ -302,6 +399,19 @@ function buildDocumentContext(
302
399
  return parts.join("\n\n");
303
400
  }
304
401
 
402
+ function buildWebSearchContext(results: WebSearchResult[]): string {
403
+ return results
404
+ .map((result, index) => {
405
+ const parts = [`${index + 1}. ${result.title ?? result.url ?? "Untitled result"}`];
406
+ if (result.url) parts.push(`URL: ${result.url}`);
407
+ if (result.summary) parts.push(`Summary: ${result.summary}`);
408
+ if (result.source) parts.push(`Source: ${result.source}`);
409
+ if (result.published_date) parts.push(`Published: ${result.published_date}`);
410
+ return parts.join("\n");
411
+ })
412
+ .join("\n\n");
413
+ }
414
+
305
415
  // ============================================================================
306
416
  // Config Parser
307
417
  // ============================================================================
@@ -320,6 +430,10 @@ const ALLOWED_KEYS = [
320
430
  "searchThreshold",
321
431
  "topK",
322
432
  "rerank",
433
+ "webSearchIncludeDomains",
434
+ "webSearchExcludeDomains",
435
+ "webSearchCountry",
436
+ "webSearchTimezone",
323
437
  ];
324
438
 
325
439
  function assertAllowedKeys(
@@ -332,6 +446,28 @@ function assertAllowedKeys(
332
446
  throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
333
447
  }
334
448
 
449
+ function parseOptionalStringArray(
450
+ value: unknown,
451
+ label: string,
452
+ ): string[] | undefined {
453
+ if (value == null) return undefined;
454
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
455
+ throw new Error(`${label} must be an array of strings`);
456
+ }
457
+ return value;
458
+ }
459
+
460
+ function parseOptionalString(
461
+ value: unknown,
462
+ label: string,
463
+ ): string | undefined {
464
+ if (value == null) return undefined;
465
+ if (typeof value !== "string") {
466
+ throw new Error(`${label} must be a string`);
467
+ }
468
+ return value;
469
+ }
470
+
335
471
  const memoryLakeConfigSchema = {
336
472
  parse(value: unknown): MemoryLakeConfig {
337
473
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -361,6 +497,22 @@ const memoryLakeConfigSchema = {
361
497
  typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.3,
362
498
  topK: typeof cfg.topK === "number" ? cfg.topK : 5,
363
499
  rerank: cfg.rerank !== false,
500
+ webSearchIncludeDomains: parseOptionalStringArray(
501
+ cfg.webSearchIncludeDomains,
502
+ "webSearchIncludeDomains",
503
+ ),
504
+ webSearchExcludeDomains: parseOptionalStringArray(
505
+ cfg.webSearchExcludeDomains,
506
+ "webSearchExcludeDomains",
507
+ ),
508
+ webSearchCountry: parseOptionalString(
509
+ cfg.webSearchCountry,
510
+ "webSearchCountry",
511
+ ),
512
+ webSearchTimezone: parseOptionalString(
513
+ cfg.webSearchTimezone,
514
+ "webSearchTimezone",
515
+ ),
364
516
  };
365
517
  },
366
518
  };
@@ -380,6 +532,52 @@ const memoryPlugin = {
380
532
  const cfg = memoryLakeConfigSchema.parse(api.pluginConfig);
381
533
  const provider: MemoryLakeProvider = new PlatformProvider(cfg.host, cfg.apiKey, cfg.projectId);
382
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
+
383
581
  // Track current session ID for tool-level session scoping
384
582
  let currentSessionId: string | undefined;
385
583
 
@@ -388,9 +586,9 @@ const memoryPlugin = {
388
586
  );
389
587
 
390
588
  // Helper: build add options
391
- function buildAddOptions(userIdOverride?: string, sessionId?: string): AddOptions {
589
+ function buildAddOptions(effectiveCfg: MemoryLakeConfig, userIdOverride?: string, sessionId?: string): AddOptions {
392
590
  const opts: AddOptions = {
393
- user_id: userIdOverride || cfg.userId,
591
+ user_id: userIdOverride || effectiveCfg.userId,
394
592
  infer: true,
395
593
  metadata: { source: "OPENCLAW" },
396
594
  };
@@ -400,14 +598,15 @@ const memoryPlugin = {
400
598
 
401
599
  // Helper: build search options
402
600
  function buildSearchOptions(
601
+ effectiveCfg: MemoryLakeConfig,
403
602
  userIdOverride?: string,
404
603
  limit?: number,
405
604
  ): SearchOptions {
406
605
  return {
407
- user_id: userIdOverride || cfg.userId,
408
- top_k: limit ?? cfg.topK,
409
- threshold: cfg.searchThreshold,
410
- rerank: cfg.rerank,
606
+ user_id: userIdOverride || effectiveCfg.userId,
607
+ top_k: limit ?? effectiveCfg.topK,
608
+ threshold: effectiveCfg.searchThreshold,
609
+ rerank: effectiveCfg.rerank,
411
610
  };
412
611
  }
413
612
 
@@ -416,7 +615,7 @@ const memoryPlugin = {
416
615
  // ========================================================================
417
616
 
418
617
  api.registerTool(
419
- {
618
+ (ctx) => ({
420
619
  name: "memory_search",
421
620
  label: "Memory Search",
422
621
  description:
@@ -446,6 +645,8 @@ const memoryPlugin = {
446
645
  ),
447
646
  }),
448
647
  async execute(_toolCallId, params) {
648
+ const effectiveCfg = resolveConfig(ctx);
649
+ const effectiveProvider = getProvider(effectiveCfg);
449
650
  const { query, limit, userId, scope = "all" } = params as {
450
651
  query: string;
451
652
  limit?: number;
@@ -454,9 +655,9 @@ const memoryPlugin = {
454
655
  };
455
656
 
456
657
  try {
457
- const results = await provider.search(
658
+ const results = await effectiveProvider.search(
458
659
  query,
459
- buildSearchOptions(userId, limit),
660
+ buildSearchOptions(effectiveCfg, userId, limit),
460
661
  );
461
662
 
462
663
  if (!results || results.length === 0) {
@@ -502,12 +703,12 @@ const memoryPlugin = {
502
703
  };
503
704
  }
504
705
  },
505
- },
706
+ }),
506
707
  { name: "memory_search" },
507
708
  );
508
709
 
509
710
  api.registerTool(
510
- {
711
+ (ctx) => ({
511
712
  name: "memory_store",
512
713
  label: "Memory Store",
513
714
  description:
@@ -526,6 +727,8 @@ const memoryPlugin = {
526
727
  ),
527
728
  }),
528
729
  async execute(_toolCallId, params) {
730
+ const effectiveCfg = resolveConfig(ctx);
731
+ const effectiveProvider = getProvider(effectiveCfg);
529
732
  const { text, userId } = params as {
530
733
  text: string;
531
734
  userId?: string;
@@ -533,9 +736,9 @@ const memoryPlugin = {
533
736
  };
534
737
 
535
738
  try {
536
- const result = await provider.add(
739
+ const result = await effectiveProvider.add(
537
740
  [{ role: "user", content: text }],
538
- buildAddOptions(userId, currentSessionId),
741
+ buildAddOptions(effectiveCfg, userId, currentSessionId),
539
742
  );
540
743
 
541
744
  const count = result.results?.length ?? 0;
@@ -566,12 +769,12 @@ const memoryPlugin = {
566
769
  };
567
770
  }
568
771
  },
569
- },
772
+ }),
570
773
  { name: "memory_store" },
571
774
  );
572
775
 
573
776
  api.registerTool(
574
- {
777
+ (ctx) => ({
575
778
  name: "memory_get",
576
779
  label: "Memory Get",
577
780
  description: "Retrieve a specific memory by its ID from MemoryLake.",
@@ -579,10 +782,12 @@ const memoryPlugin = {
579
782
  memoryId: Type.String({ description: "The memory ID to retrieve" }),
580
783
  }),
581
784
  async execute(_toolCallId, params) {
785
+ const effectiveCfg = resolveConfig(ctx);
786
+ const effectiveProvider = getProvider(effectiveCfg);
582
787
  const { memoryId } = params as { memoryId: string };
583
788
 
584
789
  try {
585
- const memory = await provider.get(memoryId);
790
+ const memory = await effectiveProvider.get(memoryId);
586
791
 
587
792
  return {
588
793
  content: [
@@ -605,12 +810,12 @@ const memoryPlugin = {
605
810
  };
606
811
  }
607
812
  },
608
- },
813
+ }),
609
814
  { name: "memory_get" },
610
815
  );
611
816
 
612
817
  api.registerTool(
613
- {
818
+ (ctx) => ({
614
819
  name: "memory_list",
615
820
  label: "Memory List",
616
821
  description:
@@ -634,11 +839,13 @@ const memoryPlugin = {
634
839
  ),
635
840
  }),
636
841
  async execute(_toolCallId, params) {
842
+ const effectiveCfg = resolveConfig(ctx);
843
+ const effectiveProvider = getProvider(effectiveCfg);
637
844
  const { userId, scope = "all" } = params as { userId?: string; scope?: "session" | "long-term" | "all" };
638
845
 
639
846
  try {
640
- const uid = userId || cfg.userId;
641
- const memories = await provider.getAll({ user_id: uid });
847
+ const uid = userId || effectiveCfg.userId;
848
+ const memories = await effectiveProvider.getAll({ user_id: uid });
642
849
 
643
850
  if (!memories || memories.length === 0) {
644
851
  return {
@@ -683,12 +890,12 @@ const memoryPlugin = {
683
890
  };
684
891
  }
685
892
  },
686
- },
893
+ }),
687
894
  { name: "memory_list" },
688
895
  );
689
896
 
690
897
  api.registerTool(
691
- {
898
+ (ctx) => ({
692
899
  name: "memory_forget",
693
900
  label: "Memory Forget",
694
901
  description:
@@ -697,10 +904,12 @@ const memoryPlugin = {
697
904
  memoryId: Type.String({ description: "Memory ID to delete" }),
698
905
  }),
699
906
  async execute(_toolCallId, params) {
907
+ const effectiveCfg = resolveConfig(ctx);
908
+ const effectiveProvider = getProvider(effectiveCfg);
700
909
  const { memoryId } = params as { memoryId: string };
701
910
 
702
911
  try {
703
- await provider.delete(memoryId);
912
+ await effectiveProvider.delete(memoryId);
704
913
  return {
705
914
  content: [
706
915
  { type: "text", text: `Memory ${memoryId} forgotten.` },
@@ -719,12 +928,12 @@ const memoryPlugin = {
719
928
  };
720
929
  }
721
930
  },
722
- },
931
+ }),
723
932
  { name: "memory_forget" },
724
933
  );
725
934
 
726
935
  api.registerTool(
727
- {
936
+ (ctx) => ({
728
937
  name: "document_search",
729
938
  label: "Document Search",
730
939
  description:
@@ -739,12 +948,14 @@ const memoryPlugin = {
739
948
  ),
740
949
  }),
741
950
  async execute(_toolCallId, params) {
951
+ const effectiveCfg = resolveConfig(ctx);
952
+ const effectiveProvider = getProvider(effectiveCfg);
742
953
  const { query, topN } = params as { query: string; topN?: number };
743
954
 
744
955
  try {
745
- const response = await provider.searchDocuments(
956
+ const response = await effectiveProvider.searchDocuments(
746
957
  query,
747
- topN ?? cfg.topK,
958
+ topN ?? effectiveCfg.topK,
748
959
  );
749
960
 
750
961
  if (!response.results || response.results.length === 0) {
@@ -779,10 +990,140 @@ const memoryPlugin = {
779
990
  };
780
991
  }
781
992
  },
782
- },
993
+ }),
783
994
  { name: "document_search" },
784
995
  );
785
996
 
997
+ api.registerTool(
998
+ (ctx) => ({
999
+ name: "advanced_web_search",
1000
+ label: "Advanced Web Search",
1001
+ description:
1002
+ "Search the web using the unified search API with plugin-level domain and location constraints. Use this for recent information, public web pages, or web research that should respect configured allowed domains, blocked domains, and user locale.",
1003
+ parameters: Type.Object({
1004
+ query: Type.String({
1005
+ description:
1006
+ "The web search query to send to the unified search endpoint.",
1007
+ }),
1008
+ domain: Type.Optional(
1009
+ Type.Union(
1010
+ [
1011
+ Type.Literal("web"),
1012
+ Type.Literal("academic"),
1013
+ Type.Literal("news"),
1014
+ Type.Literal("people"),
1015
+ Type.Literal("company"),
1016
+ Type.Literal("financial"),
1017
+ Type.Literal("markets"),
1018
+ Type.Literal("code"),
1019
+ Type.Literal("legal"),
1020
+ Type.Literal("government"),
1021
+ Type.Literal("poi"),
1022
+ Type.Literal("auto"),
1023
+ ],
1024
+ {
1025
+ description:
1026
+ "Search domain. Default: web. Invalid or unknown values are treated as auto.",
1027
+ },
1028
+ ),
1029
+ ),
1030
+ maxResults: Type.Optional(
1031
+ Type.Number({
1032
+ description: `Maximum number of web results to return (default: ${cfg.topK}).`,
1033
+ minimum: 1,
1034
+ }),
1035
+ ),
1036
+ startDate: Type.Optional(
1037
+ Type.String({
1038
+ description:
1039
+ "Only include results published on or after this date (YYYY-MM-DD).",
1040
+ }),
1041
+ ),
1042
+ endDate: Type.Optional(
1043
+ Type.String({
1044
+ description:
1045
+ "Only include results published on or before this date (YYYY-MM-DD).",
1046
+ }),
1047
+ ),
1048
+ }),
1049
+ async execute(_toolCallId, params) {
1050
+ const effectiveCfg = resolveConfig(ctx);
1051
+ const effectiveProvider = getProvider(effectiveCfg);
1052
+ const {
1053
+ query,
1054
+ domain: rawDomain,
1055
+ maxResults,
1056
+ startDate,
1057
+ endDate,
1058
+ } = params as {
1059
+ query: string;
1060
+ domain?: string;
1061
+ maxResults?: number;
1062
+ startDate?: string;
1063
+ endDate?: string;
1064
+ };
1065
+ const domain: WebSearchDomain =
1066
+ rawDomain === undefined || rawDomain === null
1067
+ ? "web"
1068
+ : normalizeWebSearchDomain(rawDomain);
1069
+
1070
+ try {
1071
+ const response = await effectiveProvider.searchWeb(query, {
1072
+ domain,
1073
+ max_results: maxResults ?? effectiveCfg.topK,
1074
+ start_date: startDate,
1075
+ end_date: endDate,
1076
+ include_domains: effectiveCfg.webSearchIncludeDomains,
1077
+ exclude_domains: effectiveCfg.webSearchExcludeDomains,
1078
+ user_location:
1079
+ effectiveCfg.webSearchCountry || effectiveCfg.webSearchTimezone
1080
+ ? {
1081
+ country: effectiveCfg.webSearchCountry,
1082
+ timezone: effectiveCfg.webSearchTimezone,
1083
+ }
1084
+ : undefined,
1085
+ });
1086
+
1087
+ if (!response.results || response.results.length === 0) {
1088
+ return {
1089
+ content: [
1090
+ { type: "text", text: "No relevant web results found." },
1091
+ ],
1092
+ details: { count: 0, total_results: response.total_results },
1093
+ };
1094
+ }
1095
+
1096
+ const context = buildWebSearchContext(response.results);
1097
+
1098
+ return {
1099
+ content: [
1100
+ {
1101
+ type: "text",
1102
+ text: `Found ${response.results.length} web results:\n\n${context}`,
1103
+ },
1104
+ ],
1105
+ details: {
1106
+ count: response.results.length,
1107
+ total_results: response.total_results,
1108
+ results: response.results,
1109
+ },
1110
+ };
1111
+ } catch (err) {
1112
+ return {
1113
+ content: [
1114
+ {
1115
+ type: "text",
1116
+ text: `Web search failed: ${String(err)}`,
1117
+ },
1118
+ ],
1119
+ details: { error: String(err) },
1120
+ };
1121
+ }
1122
+ },
1123
+ }),
1124
+ { optional: true },
1125
+ );
1126
+
786
1127
  // ========================================================================
787
1128
  // CLI Commands
788
1129
  // ========================================================================
@@ -803,7 +1144,7 @@ const memoryPlugin = {
803
1144
  const limit = parseInt(opts.limit, 10);
804
1145
  const results = await provider.search(
805
1146
  query,
806
- buildSearchOptions(undefined, limit),
1147
+ buildSearchOptions(cfg, undefined, limit),
807
1148
  );
808
1149
 
809
1150
  if (!results.length) {
@@ -855,13 +1196,17 @@ const memoryPlugin = {
855
1196
  api.on("before_agent_start", async (event, ctx) => {
856
1197
  if (!event.prompt || event.prompt.length < 5) return;
857
1198
 
1199
+ // Resolve per-workspace config override
1200
+ const effectiveCfg = resolveConfig(ctx);
1201
+ const effectiveProvider = getProvider(effectiveCfg);
1202
+
858
1203
  // Track session ID
859
1204
  const sessionId = (ctx as any)?.sessionKey ?? undefined;
860
1205
  if (sessionId) currentSessionId = sessionId;
861
1206
 
862
1207
  const [memoryResult, docResult] = await Promise.allSettled([
863
- provider.search(event.prompt, buildSearchOptions()),
864
- provider.searchDocuments(event.prompt, cfg.topK),
1208
+ effectiveProvider.search(event.prompt, buildSearchOptions(effectiveCfg)),
1209
+ effectiveProvider.searchDocuments(event.prompt, effectiveCfg.topK),
865
1210
  ]);
866
1211
 
867
1212
  const contextParts: string[] = [];
@@ -905,6 +1250,10 @@ const memoryPlugin = {
905
1250
  return;
906
1251
  }
907
1252
 
1253
+ // Resolve per-workspace config override
1254
+ const effectiveCfg = resolveConfig(ctx);
1255
+ const effectiveProvider = getProvider(effectiveCfg);
1256
+
908
1257
  // Track session ID
909
1258
  const sessionId = (ctx as any)?.sessionKey ?? undefined;
910
1259
  if (sessionId) currentSessionId = sessionId;
@@ -962,8 +1311,8 @@ const memoryPlugin = {
962
1311
 
963
1312
  if (formattedMessages.length === 0) return;
964
1313
 
965
- const addOpts = buildAddOptions(undefined, currentSessionId);
966
- const result = await provider.add(
1314
+ const addOpts = buildAddOptions(effectiveCfg, undefined, currentSessionId);
1315
+ const result = await effectiveProvider.add(
967
1316
  formattedMessages,
968
1317
  addOpts,
969
1318
  );
@@ -35,6 +35,26 @@
35
35
  "rerank": {
36
36
  "label": "Rerank",
37
37
  "help": "Rerank search results"
38
+ },
39
+ "webSearchIncludeDomains": {
40
+ "label": "Web Search Allowed Domains",
41
+ "placeholder": "[\"example.com\", \"docs.example.com\"]",
42
+ "help": "Optional allowlist for advanced_web_search results."
43
+ },
44
+ "webSearchExcludeDomains": {
45
+ "label": "Web Search Blocked Domains",
46
+ "placeholder": "[\"example.com\"]",
47
+ "help": "Optional denylist for advanced_web_search results."
48
+ },
49
+ "webSearchCountry": {
50
+ "label": "Web Search Country",
51
+ "placeholder": "US",
52
+ "help": "Optional ISO country code used to localize advanced_web_search results."
53
+ },
54
+ "webSearchTimezone": {
55
+ "label": "Web Search Timezone",
56
+ "placeholder": "America/Los_Angeles",
57
+ "help": "Optional IANA timezone used to localize advanced_web_search results."
38
58
  }
39
59
  },
40
60
  "configSchema": {
@@ -64,6 +84,24 @@
64
84
  },
65
85
  "rerank": {
66
86
  "type": "boolean"
87
+ },
88
+ "webSearchIncludeDomains": {
89
+ "type": "array",
90
+ "items": {
91
+ "type": "string"
92
+ }
93
+ },
94
+ "webSearchExcludeDomains": {
95
+ "type": "array",
96
+ "items": {
97
+ "type": "string"
98
+ }
99
+ },
100
+ "webSearchCountry": {
101
+ "type": "string"
102
+ },
103
+ "webSearchTimezone": {
104
+ "type": "string"
67
105
  }
68
106
  },
69
107
  "required": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
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