@openclaw/exa-plugin 2026.9.7 → 2026.10.1-beta.1

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.
@@ -1,34 +1,7 @@
1
1
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
2
2
  import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
3
- //#region extensions/exa/src/exa-web-search-provider.shared.ts
4
- const EXA_CREDENTIAL_PATH = "plugins.entries.exa.config.webSearch.apiKey";
5
- const EXA_ONBOARDING_SCOPES = ["text-inference"];
6
- function createExaWebSearchProviderBase() {
7
- return {
8
- id: "exa",
9
- label: "Exa Search",
10
- hint: "Neural + keyword search with date filters and content extraction",
11
- onboardingScopes: [...EXA_ONBOARDING_SCOPES],
12
- credentialLabel: "Exa API key",
13
- envVars: ["EXA_API_KEY"],
14
- placeholder: "exa-...",
15
- signupUrl: "https://exa.ai/",
16
- docsUrl: "https://docs.openclaw.ai/tools/web",
17
- autoDetectOrder: 65,
18
- credentialPath: EXA_CREDENTIAL_PATH,
19
- ...createWebSearchProviderContractFields({
20
- credentialPath: EXA_CREDENTIAL_PATH,
21
- searchCredential: {
22
- type: "scoped",
23
- scopeId: "exa"
24
- },
25
- configuredCredential: { pluginId: "exa" },
26
- selectionPluginId: "exa"
27
- })
28
- };
29
- }
30
- //#endregion
31
3
  //#region extensions/exa/src/exa-web-search-provider.ts
4
+ const EXA_CREDENTIAL_PATH = "plugins.entries.exa.config.webSearch.apiKey";
32
5
  const EXA_SEARCH_TYPES = [
33
6
  "auto",
34
7
  "neural",
@@ -44,7 +17,7 @@ const EXA_FRESHNESS_VALUES = [
44
17
  "year"
45
18
  ];
46
19
  const EXA_MAX_SEARCH_COUNT = 100;
47
- const loadExaWebSearchRuntime = createLazyRuntimeModule(() => import("./exa-web-search-provider.runtime-DE40-fMu.mjs"));
20
+ const loadExaWebSearchRuntime = createLazyRuntimeModule(() => import("./exa-web-search-provider.runtime-ClUkM3ZN.mjs"));
48
21
  const ExaSearchSchema = {
49
22
  type: "object",
50
23
  properties: {
@@ -90,7 +63,26 @@ const ExaSearchSchema = {
90
63
  };
91
64
  function createExaWebSearchProvider() {
92
65
  return {
93
- ...createExaWebSearchProviderBase(),
66
+ id: "exa",
67
+ label: "Exa Search",
68
+ hint: "Neural + keyword search with date filters and content extraction",
69
+ onboardingScopes: ["text-inference"],
70
+ credentialLabel: "Exa API key",
71
+ envVars: ["EXA_API_KEY"],
72
+ placeholder: "exa-...",
73
+ signupUrl: "https://exa.ai/",
74
+ docsUrl: "https://docs.openclaw.ai/tools/web",
75
+ autoDetectOrder: 65,
76
+ credentialPath: EXA_CREDENTIAL_PATH,
77
+ ...createWebSearchProviderContractFields({
78
+ credentialPath: EXA_CREDENTIAL_PATH,
79
+ searchCredential: {
80
+ type: "scoped",
81
+ scopeId: "exa"
82
+ },
83
+ configuredCredential: { pluginId: "exa" },
84
+ selectionPluginId: "exa"
85
+ }),
94
86
  createTool: (ctx) => ({
95
87
  description: "Search the web using Exa AI. Supports neural or keyword search, publication date filters, and optional highlights or text extraction.",
96
88
  parameters: ExaSearchSchema,
@@ -2,7 +2,7 @@ import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
2
2
  import { ProviderHttpError, readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
3
3
  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
4
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
5
- import { asOptionalObjectRecord, asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ import { asOptionalObjectRecord, asOptionalRecord, isRecord, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeTrimmedStringList } from "openclaw/plugin-sdk/string-coerce-runtime";
6
6
  //#region extensions/exa/src/exa-web-search-provider.runtime.ts
7
7
  const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
8
8
  const EXA_SEARCH_TYPES = [
@@ -25,15 +25,16 @@ const EXA_SEARCH_JSON_MAX_BYTES = 16777216;
25
25
  async function readExaSearchResults(response) {
26
26
  const bytes = await readResponseWithLimit(response, EXA_SEARCH_JSON_MAX_BYTES, { onOverflow: ({ maxBytes: maxBytesLocal }) => /* @__PURE__ */ new Error(`Exa API response exceeds ${maxBytesLocal} bytes`) });
27
27
  try {
28
- return normalizeExaResults(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
28
+ const payload = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
29
+ const results = asOptionalObjectRecord(payload)?.results;
30
+ return Array.isArray(results) ? results.filter(isRecord) : [];
29
31
  } catch (cause) {
30
32
  throw new Error("Exa API returned malformed JSON", { cause });
31
33
  }
32
34
  }
33
35
  function normalizeExaFreshness(value) {
34
36
  const trimmed = normalizeOptionalLowercaseString(value);
35
- if (!trimmed) return;
36
- return EXA_FRESHNESS_VALUES.includes(trimmed) ? trimmed : void 0;
37
+ return EXA_FRESHNESS_VALUES.find((freshness) => freshness === trimmed);
37
38
  }
38
39
  function resolveExaApiKey(exa) {
39
40
  return readConfiguredSecretString(exa?.apiKey, "plugins.entries.exa.config.webSearch.apiKey") ?? readProviderEnvValue(["EXA_API_KEY"]);
@@ -58,23 +59,10 @@ function resolveExaSearchEndpoint(exa) {
58
59
  }
59
60
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return invalidBaseUrlPayload(configured);
60
61
  const pathname = parsed.pathname.replace(/\/+$/, "");
61
- parsed.pathname = pathname.endsWith("/search") ? pathname : `${pathname === "" ? "" : pathname}/search`;
62
+ parsed.pathname = pathname.endsWith("/search") ? pathname : `${pathname}/search`;
62
63
  parsed.hash = "";
63
64
  return { endpoint: parsed.toString() };
64
65
  }
65
- function resolveExaDescription(result) {
66
- const highlights = result.highlights;
67
- if (Array.isArray(highlights)) {
68
- const highlightText = highlights.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)).join("\n");
69
- if (highlightText) return highlightText;
70
- }
71
- const summary = normalizeOptionalString(result.summary);
72
- if (summary) return summary;
73
- return normalizeOptionalString(result.text) ?? "";
74
- }
75
- function parsePositiveInteger(value) {
76
- return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
77
- }
78
66
  function invalidContentsPayload(message) {
79
67
  return {
80
68
  error: "invalid_contents",
@@ -118,7 +106,7 @@ function parseExaContents(rawContents) {
118
106
  const option = value;
119
107
  const fields = fieldsBySection[section];
120
108
  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.`}`);
121
- for (const field of fields) if (field !== "query" && field in option && parsePositiveInteger(option[field]) === void 0) return invalidContentsPayload(`contents.${section}.${field} must be a positive integer.`);
109
+ 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.`);
122
110
  if (section !== "text" && "query" in option && typeof option.query !== "string") return invalidContentsPayload(`contents.${section}.query must be a string.`);
123
111
  const normalized = {};
124
112
  for (const field of fields) if (field in option) normalized[field] = option[field];
@@ -126,10 +114,6 @@ function parseExaContents(rawContents) {
126
114
  }
127
115
  return { value: parsed };
128
116
  }
129
- function normalizeExaResults(payload) {
130
- const results = asOptionalObjectRecord(payload)?.results;
131
- return Array.isArray(results) ? results.filter(isRecord) : [];
132
- }
133
117
  function resolveFreshnessStartDate(freshness) {
134
118
  const now = /* @__PURE__ */ new Date();
135
119
  if (freshness === "day") {
@@ -151,70 +135,21 @@ function resolveFreshnessStartDate(freshness) {
151
135
  now.setUTCFullYear(now.getUTCFullYear() - 1);
152
136
  return now.toISOString();
153
137
  }
154
- async function runExaSearch(params) {
155
- const body = {
156
- query: params.query,
157
- numResults: params.count,
158
- type: params.type,
159
- contents: params.contents ?? { highlights: true }
160
- };
161
- if (params.dateAfter) body.startPublishedDate = params.dateAfter;
162
- else if (params.freshness) body.startPublishedDate = resolveFreshnessStartDate(params.freshness);
163
- if (params.dateBefore) body.endPublishedDate = params.dateBefore;
164
- return withTrustedWebSearchEndpoint({
165
- url: params.endpoint,
166
- timeoutSeconds: params.timeoutSeconds,
167
- signal: params.signal,
168
- init: {
169
- method: "POST",
170
- headers: {
171
- Accept: "application/json",
172
- "Content-Type": "application/json",
173
- "x-api-key": params.apiKey,
174
- "x-exa-integration": "openclaw"
175
- },
176
- body: JSON.stringify(body)
177
- }
178
- }, async (res) => {
179
- if (!res.ok) {
180
- const detail = await readResponseTextLimited(res, EXA_ERROR_BODY_LIMIT_BYTES);
181
- throw new ProviderHttpError(`Exa API error (${res.status}): ${detail || res.statusText}`, { status: res.status });
182
- }
183
- return (await readExaSearchResults(res)).slice(0, params.count);
184
- });
185
- }
186
- function missingExaKeyPayload() {
187
- return {
188
- error: "missing_exa_api_key",
189
- 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.",
190
- docs: "https://docs.openclaw.ai/tools/web"
191
- };
192
- }
193
- function buildExaCacheKey(params) {
194
- const contents = params.contents ?? { highlights: true };
195
- return buildSearchCacheKey([
196
- "exa",
197
- params.endpoint,
198
- params.type,
199
- params.query,
200
- params.count,
201
- params.freshness,
202
- params.dateAfter,
203
- params.dateBefore,
204
- JSON.stringify(contents)
205
- ]);
206
- }
207
138
  async function executeExaWebSearchProviderTool(ctx, args, signal) {
208
139
  const searchConfig = mergeScopedSearchConfig(ctx.searchConfig, "exa", resolveProviderWebSearchPluginConfig(ctx.config, "exa"));
209
140
  const exaConfig = asOptionalRecord(searchConfig?.exa);
210
141
  const apiKey = resolveExaApiKey(exaConfig);
211
- if (!apiKey) return missingExaKeyPayload();
142
+ if (!apiKey) return {
143
+ error: "missing_exa_api_key",
144
+ 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.",
145
+ docs: "https://docs.openclaw.ai/tools/web"
146
+ };
212
147
  const endpointResult = resolveExaSearchEndpoint(exaConfig);
213
148
  if ("error" in endpointResult) return endpointResult;
214
149
  const endpoint = endpointResult.endpoint;
215
150
  const query = readStringParam(args, "query", { required: true });
216
151
  const rawType = readStringParam(args, "type");
217
- const type = EXA_SEARCH_TYPES.includes(rawType) ? rawType : "auto";
152
+ const type = EXA_SEARCH_TYPES.find((candidate) => candidate === rawType) ?? "auto";
218
153
  const count = readPositiveIntegerParam(args, "count", {
219
154
  max: EXA_MAX_SEARCH_COUNT,
220
155
  message: `count must be an integer from 1 to ${EXA_MAX_SEARCH_COUNT}.`
@@ -246,32 +181,51 @@ async function executeExaWebSearchProviderTool(ctx, args, signal) {
246
181
  if ("error" in parsedContents) return parsedContents;
247
182
  const contents = parsedContents.value && Object.keys(parsedContents.value).length > 0 ? parsedContents.value : void 0;
248
183
  const resolvedCount = Math.min(EXA_MAX_SEARCH_COUNT, parseStrictPositiveInteger(count) ?? DEFAULT_SEARCH_COUNT);
249
- const cacheKey = buildExaCacheKey({
184
+ const cacheKey = buildSearchCacheKey([
185
+ "exa",
250
186
  endpoint,
251
187
  type,
252
188
  query,
253
- count: resolvedCount,
189
+ resolvedCount,
254
190
  freshness,
255
191
  dateAfter,
256
192
  dateBefore,
257
- contents
258
- });
193
+ JSON.stringify(contents ?? { highlights: true })
194
+ ]);
259
195
  const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
260
196
  const cached = readCachedSearchPayload(cacheKey, cacheTtlMs);
261
197
  if (cached) return cached;
262
198
  const start = Date.now();
263
- const results = await runExaSearch({
264
- apiKey,
265
- endpoint,
199
+ const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
200
+ const body = {
266
201
  query,
267
- count: resolvedCount,
268
- freshness,
269
- dateAfter,
270
- dateBefore,
202
+ numResults: resolvedCount,
271
203
  type,
272
- contents,
273
- timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig),
274
- signal
204
+ contents: contents ?? { highlights: true }
205
+ };
206
+ if (dateAfter) body.startPublishedDate = dateAfter;
207
+ else if (freshness) body.startPublishedDate = resolveFreshnessStartDate(freshness);
208
+ if (dateBefore) body.endPublishedDate = dateBefore;
209
+ const results = await withTrustedWebSearchEndpoint({
210
+ url: endpoint,
211
+ timeoutSeconds,
212
+ signal,
213
+ init: {
214
+ method: "POST",
215
+ headers: {
216
+ Accept: "application/json",
217
+ "Content-Type": "application/json",
218
+ "x-api-key": apiKey,
219
+ "x-exa-integration": "openclaw"
220
+ },
221
+ body: JSON.stringify(body)
222
+ }
223
+ }, async (res) => {
224
+ if (!res.ok) {
225
+ const detail = await readResponseTextLimited(res, EXA_ERROR_BODY_LIMIT_BYTES);
226
+ throw new ProviderHttpError(`Exa API error (${res.status}): ${detail || res.statusText}`, { status: res.status });
227
+ }
228
+ return (await readExaSearchResults(res)).slice(0, resolvedCount);
275
229
  });
276
230
  signal?.throwIfAborted();
277
231
  const payload = {
@@ -288,7 +242,7 @@ async function executeExaWebSearchProviderTool(ctx, args, signal) {
288
242
  results: results.map((entry) => {
289
243
  const title = typeof entry.title === "string" ? entry.title : "";
290
244
  const url = typeof entry.url === "string" ? entry.url : "";
291
- const description = resolveExaDescription(entry);
245
+ const description = normalizeTrimmedStringList(entry.highlights).join("\n") || normalizeOptionalString(entry.summary) || normalizeOptionalString(entry.text) || "";
292
246
  const summary = normalizeOptionalString(entry.summary) ?? "";
293
247
  const highlightScores = Array.isArray(entry.highlightScores) ? entry.highlightScores.filter((score) => typeof score === "number" && Number.isFinite(score)) : [];
294
248
  const published = typeof entry.publishedDate === "string" && entry.publishedDate ? entry.publishedDate : void 0;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-DgKhV-9B.mjs";
1
+ import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-DkWJpeaV.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,3 @@
1
- import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-DgKhV-9B.mjs";
1
+ import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-DkWJpeaV.mjs";
2
2
  import "./web-search-provider.js";
3
3
  export { createExaWebSearchProvider };
@@ -1,2 +1,2 @@
1
- import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-DgKhV-9B.mjs";
1
+ import { t as createExaWebSearchProvider } from "./.setup/exa-web-search-provider-DkWJpeaV.mjs";
2
2
  export { createExaWebSearchProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/exa-plugin",
3
- "version": "2026.9.7",
3
+ "version": "2026.10.1-beta.1",
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.9.7"
21
+ "pluginApi": ">=2026.10.1-beta.1"
22
22
  },
23
23
  "build": {
24
- "openclawVersion": "2026.9.7",
24
+ "openclawVersion": "2026.10.1-beta.1",
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.9.7"
43
+ "openclaw": ">=2026.10.1-beta.1"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "openclaw": {