@openclaw/exa-plugin 2026.11.3 → 2026.11.5-beta.2

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.
@@ -17,7 +17,7 @@ const EXA_FRESHNESS_VALUES = [
17
17
  "year"
18
18
  ];
19
19
  const EXA_MAX_SEARCH_COUNT = 100;
20
- const loadExaWebSearchRuntime = createLazyRuntimeModule(() => import("./exa-web-search-provider.runtime-C73kkrxv.mjs"));
20
+ const loadExaWebSearchRuntime = createLazyRuntimeModule(() => import("./exa-web-search-provider.runtime-DgICTEId.mjs"));
21
21
  const ExaSearchSchema = {
22
22
  type: "object",
23
23
  properties: {
@@ -1,7 +1,7 @@
1
- import { createHash } from "node:crypto";
1
+ import { sha256Hex } from "openclaw/plugin-sdk/node-crypto-runtime";
2
2
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
3
3
  import { ProviderHttpError, readProviderResponseErrorText, redactProviderResponseErrorText } from "openclaw/plugin-sdk/provider-http";
4
- import { DEFAULT_SEARCH_COUNT, buildSearchCacheKey, mergeScopedSearchConfig, parseIsoDateRange, readCachedSearchPayload, readConfiguredSecretString, readPositiveIntegerParam, readProviderEnvValue, readStringParam, resolveProviderWebSearchPluginConfig, resolveSearchCacheTtlMs, resolveSearchTimeoutSeconds, resolveSiteName, withTrustedWebSearchEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
4
+ import { DEFAULT_SEARCH_COUNT, buildSearchCacheKey, externalContentStamp, mergeScopedSearchConfig, parseIsoDateRange, readCachedSearchPayload, readPositiveIntegerParam, readStringParam, readWebSearchApiKey, resolveProviderWebSearchPluginConfig, resolveSearchCacheTtlMs, resolveSearchTimeoutSeconds, resolveSiteName, withTrustedWebSearchEndpoint, wrapWebContent, writeCachedSearchPayload } from "openclaw/plugin-sdk/provider-web-search";
5
5
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
6
6
  import { asOptionalObjectRecord, asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
7
7
  //#region extensions/exa/src/exa-web-search-provider.runtime.ts
@@ -23,23 +23,6 @@ const EXA_FRESHNESS_VALUES = [
23
23
  const EXA_MAX_SEARCH_COUNT = 100;
24
24
  const EXA_ERROR_BODY_LIMIT_BYTES = 8192;
25
25
  const EXA_SEARCH_JSON_MAX_BYTES = 16777216;
26
- async function readExaSearchResults(response) {
27
- const bytes = await readResponseWithLimit(response, EXA_SEARCH_JSON_MAX_BYTES, { onOverflow: ({ maxBytes: maxBytesLocal }) => /* @__PURE__ */ new Error(`Exa API response exceeds ${maxBytesLocal} bytes`) });
28
- try {
29
- const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
30
- const results = asOptionalObjectRecord(payload)?.results;
31
- return Array.isArray(results) ? results.filter(isRecord) : [];
32
- } catch (cause) {
33
- throw new Error("Exa API returned malformed JSON", { cause });
34
- }
35
- }
36
- function normalizeExaFreshness(value) {
37
- const trimmed = normalizeOptionalLowercaseString(value);
38
- return EXA_FRESHNESS_VALUES.find((freshness) => freshness === trimmed);
39
- }
40
- function resolveExaApiKey(exa) {
41
- return readConfiguredSecretString(exa?.apiKey, "plugins.entries.exa.config.webSearch.apiKey") ?? readProviderEnvValue(["EXA_API_KEY"]);
42
- }
43
26
  function invalidBaseUrlPayload() {
44
27
  return {
45
28
  error: "invalid_base_url",
@@ -47,23 +30,6 @@ function invalidBaseUrlPayload() {
47
30
  docs: "https://docs.openclaw.ai/tools/exa-search"
48
31
  };
49
32
  }
50
- function resolveExaSearchEndpoint(exa) {
51
- const configured = normalizeOptionalString(exa?.baseUrl);
52
- if (!configured) return { endpoint: EXA_SEARCH_ENDPOINT };
53
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(configured) && !/^https?:\/\//i.test(configured)) return invalidBaseUrlPayload();
54
- const candidate = /^https?:\/\//i.test(configured) ? configured : `https://${configured}`;
55
- let parsed;
56
- try {
57
- parsed = new URL(candidate);
58
- } catch {
59
- return invalidBaseUrlPayload();
60
- }
61
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return invalidBaseUrlPayload();
62
- const pathname = parsed.pathname.replace(/\/+$/, "");
63
- parsed.pathname = pathname.endsWith("/search") ? pathname : `${pathname}/search`;
64
- parsed.hash = "";
65
- return { endpoint: parsed.toString() };
66
- }
67
33
  function invalidContentsPayload(message) {
68
34
  return {
69
35
  error: "invalid_contents",
@@ -71,92 +37,42 @@ function invalidContentsPayload(message) {
71
37
  docs: "https://docs.openclaw.ai/tools/web"
72
38
  };
73
39
  }
74
- function parseExaContents(rawContents) {
75
- if (rawContents === void 0) return { value: void 0 };
76
- if (!isRecord(rawContents)) return invalidContentsPayload("contents must be an object with optional text, highlights, and summary fields.");
77
- const raw = rawContents;
78
- const allowedKeys = /* @__PURE__ */ new Set([
79
- "text",
80
- "highlights",
81
- "summary"
82
- ]);
83
- for (const key of Object.keys(raw)) if (!allowedKeys.has(key)) return invalidContentsPayload(`contents has unknown field "${key}". Only "text", "highlights", and "summary" are allowed.`);
84
- const parsed = {};
85
- const fieldsBySection = {
86
- text: ["maxCharacters"],
87
- highlights: [
88
- "maxCharacters",
89
- "query",
90
- "numSentences",
91
- "highlightsPerUrl"
92
- ],
93
- summary: ["query"]
94
- };
95
- for (const section of [
96
- "text",
97
- "highlights",
98
- "summary"
99
- ]) {
100
- if (!(section in raw)) continue;
101
- const value = raw[section];
102
- if (typeof value === "boolean") {
103
- parsed[section] = value;
104
- continue;
105
- }
106
- if (!isRecord(value)) return invalidContentsPayload(`contents.${section} must be a boolean or an object.`);
107
- const option = value;
108
- const fields = fieldsBySection[section];
109
- for (const key of Object.keys(option)) if (!fields.includes(key)) return invalidContentsPayload(`contents.${section} has unknown field "${key}". ${section === "highlights" ? "Allowed fields are \"maxCharacters\", \"query\", \"numSentences\", and \"highlightsPerUrl\"." : `Only "${fields[0]}" is allowed.`}`);
110
- for (const field of fields) if (field !== "query" && field in option && !(typeof option[field] === "number" && Number.isInteger(option[field]) && option[field] > 0)) return invalidContentsPayload(`contents.${section}.${field} must be a positive integer.`);
111
- if (section !== "text" && "query" in option && typeof option.query !== "string") return invalidContentsPayload(`contents.${section}.query must be a string.`);
112
- const normalized = {};
113
- for (const field of fields) if (field in option) normalized[field] = option[field];
114
- Object.assign(parsed, { [section]: normalized });
115
- }
116
- return { value: parsed };
117
- }
118
- function resolveFreshnessStartDate(freshness) {
119
- const now = /* @__PURE__ */ new Date();
120
- if (freshness === "day") {
121
- now.setUTCDate(now.getUTCDate() - 1);
122
- return now.toISOString();
123
- }
124
- if (freshness === "week") {
125
- now.setUTCDate(now.getUTCDate() - 7);
126
- return now.toISOString();
127
- }
128
- if (freshness === "month") {
129
- const currentDay = now.getUTCDate();
130
- now.setUTCDate(1);
131
- now.setUTCMonth(now.getUTCMonth() - 1);
132
- const lastDayOfTargetMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
133
- now.setUTCDate(Math.min(currentDay, lastDayOfTargetMonth));
134
- return now.toISOString();
135
- }
136
- now.setUTCFullYear(now.getUTCFullYear() - 1);
137
- return now.toISOString();
138
- }
139
40
  async function executeExaWebSearchProviderTool(ctx, args, signal) {
140
41
  const searchConfig = mergeScopedSearchConfig(ctx.searchConfig, "exa", resolveProviderWebSearchPluginConfig(ctx.config, "exa"));
141
42
  const exaConfig = asOptionalRecord(searchConfig?.exa);
142
- const apiKey = resolveExaApiKey(exaConfig);
43
+ const apiKey = readWebSearchApiKey(exaConfig?.apiKey, "plugins.entries.exa.config.webSearch.apiKey", ["EXA_API_KEY"]);
143
44
  if (!apiKey) return {
144
45
  error: "missing_exa_api_key",
145
46
  message: "web_search (exa) needs an Exa API key. Set EXA_API_KEY in the Gateway environment, or configure plugins.entries.exa.config.webSearch.apiKey.",
146
47
  docs: "https://docs.openclaw.ai/tools/web"
147
48
  };
148
- const endpointResult = resolveExaSearchEndpoint(exaConfig);
149
- if ("error" in endpointResult) return endpointResult;
150
- const endpoint = endpointResult.endpoint;
49
+ let endpoint = EXA_SEARCH_ENDPOINT;
50
+ const configured = normalizeOptionalString(exaConfig?.baseUrl);
51
+ if (configured) {
52
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(configured) && !/^https?:\/\//i.test(configured)) return invalidBaseUrlPayload();
53
+ const candidate = /^https?:\/\//i.test(configured) ? configured : `https://${configured}`;
54
+ let parsed;
55
+ try {
56
+ parsed = new URL(candidate);
57
+ } catch {
58
+ return invalidBaseUrlPayload();
59
+ }
60
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return invalidBaseUrlPayload();
61
+ const pathname = parsed.pathname.replace(/\/+$/, "");
62
+ parsed.pathname = pathname.endsWith("/search") ? pathname : `${pathname}/search`;
63
+ parsed.hash = "";
64
+ endpoint = parsed.toString();
65
+ }
151
66
  const query = readStringParam(args, "query", { required: true });
152
67
  const rawType = readStringParam(args, "type");
153
68
  const type = EXA_SEARCH_TYPES.find((candidate) => candidate === rawType) ?? "auto";
154
69
  const count = readPositiveIntegerParam(args, "count", {
155
70
  max: EXA_MAX_SEARCH_COUNT,
156
71
  message: `count must be an integer from 1 to ${EXA_MAX_SEARCH_COUNT}.`
157
- }) ?? searchConfig?.maxResults ?? void 0;
72
+ }) ?? searchConfig?.maxResults;
158
73
  const rawFreshness = readStringParam(args, "freshness");
159
- const freshness = normalizeExaFreshness(rawFreshness);
74
+ const normalizedFreshness = normalizeOptionalLowercaseString(rawFreshness);
75
+ const freshness = EXA_FRESHNESS_VALUES.find((value) => value === normalizedFreshness);
160
76
  if (rawFreshness && !freshness) return {
161
77
  error: "invalid_freshness",
162
78
  message: "freshness must be one of \"day\", \"week\", \"month\", or \"year\".",
@@ -178,13 +94,53 @@ async function executeExaWebSearchProviderTool(ctx, args, signal) {
178
94
  });
179
95
  if ("error" in parsedDateRange) return parsedDateRange;
180
96
  const { dateAfter, dateBefore } = parsedDateRange;
181
- const parsedContents = parseExaContents(args.contents);
182
- if ("error" in parsedContents) return parsedContents;
183
- const contents = parsedContents.value && Object.keys(parsedContents.value).length > 0 ? parsedContents.value : void 0;
97
+ const parsedContents = {};
98
+ const rawContents = args.contents;
99
+ if (rawContents !== void 0) {
100
+ if (!isRecord(rawContents)) return invalidContentsPayload("contents must be an object with optional text, highlights, and summary fields.");
101
+ const allowedKeys = /* @__PURE__ */ new Set([
102
+ "text",
103
+ "highlights",
104
+ "summary"
105
+ ]);
106
+ for (const key of Object.keys(rawContents)) if (!allowedKeys.has(key)) return invalidContentsPayload(`contents has unknown field "${key}". Only "text", "highlights", and "summary" are allowed.`);
107
+ const fieldsBySection = {
108
+ text: ["maxCharacters"],
109
+ highlights: [
110
+ "maxCharacters",
111
+ "query",
112
+ "numSentences",
113
+ "highlightsPerUrl"
114
+ ],
115
+ summary: ["query"]
116
+ };
117
+ for (const section of [
118
+ "text",
119
+ "highlights",
120
+ "summary"
121
+ ]) {
122
+ if (!(section in rawContents)) continue;
123
+ const value = rawContents[section];
124
+ if (typeof value === "boolean") {
125
+ parsedContents[section] = value;
126
+ continue;
127
+ }
128
+ if (!isRecord(value)) return invalidContentsPayload(`contents.${section} must be a boolean or an object.`);
129
+ const option = value;
130
+ const fields = fieldsBySection[section];
131
+ for (const key of Object.keys(option)) if (!fields.includes(key)) return invalidContentsPayload(`contents.${section} has unknown field "${key}". ${section === "highlights" ? "Allowed fields are \"maxCharacters\", \"query\", \"numSentences\", and \"highlightsPerUrl\"." : `Only "${fields[0]}" is allowed.`}`);
132
+ for (const field of fields) if (field !== "query" && field in option && !(typeof option[field] === "number" && Number.isInteger(option[field]) && option[field] > 0)) return invalidContentsPayload(`contents.${section}.${field} must be a positive integer.`);
133
+ if (section !== "text" && "query" in option && typeof option.query !== "string") return invalidContentsPayload(`contents.${section}.query must be a string.`);
134
+ const normalized = {};
135
+ for (const field of fields) if (field in option) normalized[field] = option[field];
136
+ Object.assign(parsedContents, { [section]: normalized });
137
+ }
138
+ }
139
+ const contents = Object.keys(parsedContents).length > 0 ? parsedContents : void 0;
184
140
  const resolvedCount = Math.min(EXA_MAX_SEARCH_COUNT, parseStrictPositiveInteger(count) ?? DEFAULT_SEARCH_COUNT);
185
141
  const cacheKey = buildSearchCacheKey([
186
142
  "exa",
187
- createHash("sha256").update(apiKey).digest("hex"),
143
+ sha256Hex(apiKey),
188
144
  endpoint,
189
145
  type,
190
146
  query,
@@ -206,7 +162,19 @@ async function executeExaWebSearchProviderTool(ctx, args, signal) {
206
162
  contents: contents ?? { highlights: true }
207
163
  };
208
164
  if (dateAfter) body.startPublishedDate = dateAfter;
209
- else if (freshness) body.startPublishedDate = resolveFreshnessStartDate(freshness);
165
+ else if (freshness) {
166
+ const now = /* @__PURE__ */ new Date();
167
+ if (freshness === "day") now.setUTCDate(now.getUTCDate() - 1);
168
+ else if (freshness === "week") now.setUTCDate(now.getUTCDate() - 7);
169
+ else if (freshness === "month") {
170
+ const currentDay = now.getUTCDate();
171
+ now.setUTCDate(1);
172
+ now.setUTCMonth(now.getUTCMonth() - 1);
173
+ const lastDayOfTargetMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
174
+ now.setUTCDate(Math.min(currentDay, lastDayOfTargetMonth));
175
+ } else now.setUTCFullYear(now.getUTCFullYear() - 1);
176
+ body.startPublishedDate = now.toISOString();
177
+ }
210
178
  if (dateBefore) body.endPublishedDate = dateBefore;
211
179
  const headers = {
212
180
  Accept: "application/json",
@@ -228,7 +196,14 @@ async function executeExaWebSearchProviderTool(ctx, args, signal) {
228
196
  const detail = await readProviderResponseErrorText(res, EXA_ERROR_BODY_LIMIT_BYTES, headers, signal);
229
197
  throw new ProviderHttpError(`Exa API error (${res.status}): ${detail || redactProviderResponseErrorText(res.statusText, headers)}`, { status: res.status });
230
198
  }
231
- return (await readExaSearchResults(res)).slice(0, resolvedCount);
199
+ const bytes = await readResponseWithLimit(res, EXA_SEARCH_JSON_MAX_BYTES, { onOverflow: ({ maxBytes: maxBytesLocal }) => /* @__PURE__ */ new Error(`Exa API response exceeds ${maxBytesLocal} bytes`) });
200
+ try {
201
+ const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
202
+ const rows = asOptionalObjectRecord(payload)?.results;
203
+ return (Array.isArray(rows) ? rows.filter(isRecord) : []).slice(0, resolvedCount);
204
+ } catch (cause) {
205
+ throw new Error("Exa API returned malformed JSON", { cause });
206
+ }
232
207
  });
233
208
  signal?.throwIfAborted();
234
209
  const payload = {
@@ -236,12 +211,7 @@ async function executeExaWebSearchProviderTool(ctx, args, signal) {
236
211
  provider: "exa",
237
212
  count: results.length,
238
213
  tookMs: Date.now() - start,
239
- externalContent: {
240
- untrusted: true,
241
- source: "web_search",
242
- provider: "exa",
243
- wrapped: true
244
- },
214
+ externalContent: externalContentStamp("exa"),
245
215
  results: results.map((entry) => {
246
216
  const title = typeof entry.title === "string" ? entry.title : "";
247
217
  const url = typeof entry.url === "string" ? entry.url : "";
@@ -0,0 +1 @@
1
+ {"version":3,"entries":[{"output":"index.js","source":"extensions/exa/index.ts"},{"output":"web-search-contract-api.js","source":"extensions/exa/web-search-contract-api.ts"},{"output":"web-search-provider.js","source":"extensions/exa/web-search-provider.ts"}],"chunks":[{"output":".setup/exa-web-search-provider-D2HeogPB.mjs","sha256":"0376e59d945efe6e5dbef89207a011a0a51759b809d3bd91528af9a5c1fed5d4","facadeSource":null,"sources":["extensions/exa/src/exa-web-search-provider.ts"]},{"output":".setup/exa-web-search-provider.runtime-DgICTEId.mjs","sha256":"c748c42f12fbd97cba1a6d541950ba07a1618d18e4b314aebb25727b5aeb8439","facadeSource":"extensions/exa/src/exa-web-search-provider.runtime.ts","sources":["extensions/exa/src/exa-web-search-provider.runtime.ts"]},{"output":"index.js","sha256":"703c4ed77aa96f5957c8c49249c31530654d9d746e8c7639641184bdb0a00874","facadeSource":"extensions/exa/index.ts","sources":["extensions/exa/index.ts"]},{"output":"web-search-contract-api.js","sha256":"1971df1282041c8023828af88b4ce58e91c068917b10fd16ded2fd71dbc6ac42","facadeSource":"extensions/exa/web-search-contract-api.ts","sources":[]},{"output":"web-search-provider.js","sha256":"2b368e8fb211191494aaf0db90c8478a66c86bcd99368e2c1f09b75560f96f59","facadeSource":"extensions/exa/web-search-provider.ts","sources":[]}],"unrenderedSources":["extensions/exa/web-search-contract-api.ts","extensions/exa/web-search-provider.ts"]}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-UdcmLdNp.mjs";
1
+ import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-D2HeogPB.mjs";
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
3
  //#region extensions/exa/index.ts
4
4
  var exa_default = definePluginEntry({
@@ -1,3 +1,2 @@
1
- import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-UdcmLdNp.mjs";
1
+ export { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-D2HeogPB.mjs";
2
2
  import "./web-search-provider.js";
3
- export { createExaWebSearchProvider };
@@ -1,2 +1 @@
1
- import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-UdcmLdNp.mjs";
2
- export { createExaWebSearchProvider };
1
+ export { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-D2HeogPB.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/exa-plugin",
3
- "version": "2026.11.3",
3
+ "version": "2026.11.5-beta.2",
4
4
  "description": "OpenClaw Exa plugin.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,10 +18,10 @@
18
18
  "minHostVersion": ">=2026.6.8"
19
19
  },
20
20
  "compat": {
21
- "pluginApi": ">=2026.11.3"
21
+ "pluginApi": ">=2026.11.5-beta.2"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.11.3",
24
+ "openclawVersion": "2026.11.5-beta.2",
25
25
  "bundledDist": false
26
26
  },
27
27
  "release": {
@@ -40,7 +40,7 @@
40
40
  "assets/activity.svg"
41
41
  ],
42
42
  "peerDependencies": {
43
- "openclaw": ">=2026.11.3"
43
+ "openclaw": ">=2026.11.5-beta.2"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "openclaw": {