@openclaw/exa-plugin 0.0.0 → 2026.6.9
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 +11 -2
- package/dist/exa-web-search-provider-q-LG8WsD.js +80 -0
- package/dist/exa-web-search-provider.runtime-BzWlE73e.js +356 -0
- package/dist/exa-web-search-provider.shared-CWlikD_3.js +30 -0
- package/dist/index.js +13 -0
- package/dist/web-search-contract-api.js +10 -0
- package/dist/web-search-provider.js +2 -0
- package/npm-shrinkwrap.json +12 -0
- package/openclaw.plugin.json +50 -0
- package/package.json +44 -8
package/README.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenClaw Exa Plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official OpenClaw plugin for Exa.
|
|
4
|
+
|
|
5
|
+
Install from OpenClaw:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @openclaw/exa-plugin
|
|
9
|
+
openclaw gateway restart
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
See <https://docs.openclaw.ai/tools/exa-search> for setup and configuration.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { t as createExaWebSearchProviderBase } from "./exa-web-search-provider.shared-CWlikD_3.js";
|
|
2
|
+
//#region extensions/exa/src/exa-web-search-provider.ts
|
|
3
|
+
const EXA_SEARCH_TYPES = [
|
|
4
|
+
"auto",
|
|
5
|
+
"neural",
|
|
6
|
+
"fast",
|
|
7
|
+
"deep",
|
|
8
|
+
"deep-reasoning",
|
|
9
|
+
"instant"
|
|
10
|
+
];
|
|
11
|
+
const EXA_FRESHNESS_VALUES = [
|
|
12
|
+
"day",
|
|
13
|
+
"week",
|
|
14
|
+
"month",
|
|
15
|
+
"year"
|
|
16
|
+
];
|
|
17
|
+
const EXA_MAX_SEARCH_COUNT = 100;
|
|
18
|
+
let exaWebSearchRuntimePromise;
|
|
19
|
+
function loadExaWebSearchRuntime() {
|
|
20
|
+
exaWebSearchRuntimePromise ??= import("./exa-web-search-provider.runtime-BzWlE73e.js");
|
|
21
|
+
return exaWebSearchRuntimePromise;
|
|
22
|
+
}
|
|
23
|
+
const ExaSearchSchema = {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
query: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Search query string."
|
|
29
|
+
},
|
|
30
|
+
count: {
|
|
31
|
+
type: "integer",
|
|
32
|
+
description: "Number of results to return (1-100, subject to Exa search-type limits).",
|
|
33
|
+
minimum: 1,
|
|
34
|
+
maximum: EXA_MAX_SEARCH_COUNT
|
|
35
|
+
},
|
|
36
|
+
freshness: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: [...EXA_FRESHNESS_VALUES],
|
|
39
|
+
description: "Filter by time: \"day\", \"week\", \"month\", or \"year\"."
|
|
40
|
+
},
|
|
41
|
+
date_after: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Only results published after this date (YYYY-MM-DD)."
|
|
44
|
+
},
|
|
45
|
+
date_before: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Only results published before this date (YYYY-MM-DD)."
|
|
48
|
+
},
|
|
49
|
+
type: {
|
|
50
|
+
type: "string",
|
|
51
|
+
enum: [...EXA_SEARCH_TYPES],
|
|
52
|
+
description: "Exa search mode: \"auto\", \"neural\", \"fast\", \"deep\", \"deep-reasoning\", or \"instant\"."
|
|
53
|
+
},
|
|
54
|
+
contents: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
highlights: { description: "Highlights config: true, or an object with maxCharacters, query, numSentences, or highlightsPerUrl." },
|
|
58
|
+
text: { description: "Text config: true, or an object with maxCharacters." },
|
|
59
|
+
summary: { description: "Summary config: true, or an object with query." }
|
|
60
|
+
},
|
|
61
|
+
additionalProperties: false
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
additionalProperties: false
|
|
65
|
+
};
|
|
66
|
+
function createExaWebSearchProvider() {
|
|
67
|
+
return {
|
|
68
|
+
...createExaWebSearchProviderBase(),
|
|
69
|
+
createTool: (ctx) => ({
|
|
70
|
+
description: "Search the web using Exa AI. Supports neural or keyword search, publication date filters, and optional highlights or text extraction.",
|
|
71
|
+
parameters: ExaSearchSchema,
|
|
72
|
+
execute: async (args) => {
|
|
73
|
+
const { executeExaWebSearchProviderTool } = await loadExaWebSearchRuntime();
|
|
74
|
+
return await executeExaWebSearchProviderTool(ctx, args);
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { createExaWebSearchProvider as t };
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
2
|
+
import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http";
|
|
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
|
+
import { normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
5
|
+
//#region extensions/exa/src/exa-web-search-provider.runtime.ts
|
|
6
|
+
const EXA_SEARCH_ENDPOINT = "https://api.exa.ai/search";
|
|
7
|
+
const EXA_SEARCH_TYPES = [
|
|
8
|
+
"auto",
|
|
9
|
+
"neural",
|
|
10
|
+
"fast",
|
|
11
|
+
"deep",
|
|
12
|
+
"deep-reasoning",
|
|
13
|
+
"instant"
|
|
14
|
+
];
|
|
15
|
+
const EXA_FRESHNESS_VALUES = [
|
|
16
|
+
"day",
|
|
17
|
+
"week",
|
|
18
|
+
"month",
|
|
19
|
+
"year"
|
|
20
|
+
];
|
|
21
|
+
const EXA_MAX_SEARCH_COUNT = 100;
|
|
22
|
+
const EXA_ERROR_BODY_LIMIT_BYTES = 8 * 1024;
|
|
23
|
+
async function readExaSearchResults(response) {
|
|
24
|
+
try {
|
|
25
|
+
return normalizeExaResults(await response.json());
|
|
26
|
+
} catch (cause) {
|
|
27
|
+
throw new Error("Exa API returned malformed JSON", { cause });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function readExaErrorDetail(response) {
|
|
31
|
+
return await readResponseTextLimited(response, EXA_ERROR_BODY_LIMIT_BYTES);
|
|
32
|
+
}
|
|
33
|
+
function normalizeExaFreshness(value) {
|
|
34
|
+
const trimmed = normalizeOptionalLowercaseString(value);
|
|
35
|
+
if (!trimmed) return;
|
|
36
|
+
return EXA_FRESHNESS_VALUES.includes(trimmed) ? trimmed : void 0;
|
|
37
|
+
}
|
|
38
|
+
function resolveExaConfig(searchConfig) {
|
|
39
|
+
const exa = searchConfig?.exa;
|
|
40
|
+
return exa && typeof exa === "object" && !Array.isArray(exa) ? exa : {};
|
|
41
|
+
}
|
|
42
|
+
function resolveExaApiKey(exa) {
|
|
43
|
+
return readConfiguredSecretString(exa?.apiKey, "tools.web.search.exa.apiKey") ?? readProviderEnvValue(["EXA_API_KEY"]);
|
|
44
|
+
}
|
|
45
|
+
function invalidBaseUrlPayload(value) {
|
|
46
|
+
return {
|
|
47
|
+
error: "invalid_base_url",
|
|
48
|
+
message: `plugins.entries.exa.config.webSearch.baseUrl must be a valid http(s) URL. Got: ${value}`,
|
|
49
|
+
docs: "https://docs.openclaw.ai/tools/exa-search"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function resolveExaSearchEndpoint(exa) {
|
|
53
|
+
const configured = normalizeOptionalString(exa?.baseUrl);
|
|
54
|
+
if (!configured) return { endpoint: EXA_SEARCH_ENDPOINT };
|
|
55
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(configured) && !/^https?:\/\//i.test(configured)) return invalidBaseUrlPayload(configured);
|
|
56
|
+
const candidate = /^https?:\/\//i.test(configured) ? configured : `https://${configured}`;
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = new URL(candidate);
|
|
60
|
+
} catch {
|
|
61
|
+
return invalidBaseUrlPayload(configured);
|
|
62
|
+
}
|
|
63
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return invalidBaseUrlPayload(configured);
|
|
64
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
65
|
+
parsed.pathname = pathname.endsWith("/search") ? pathname : `${pathname === "" ? "" : pathname}/search`;
|
|
66
|
+
parsed.hash = "";
|
|
67
|
+
return { endpoint: parsed.toString() };
|
|
68
|
+
}
|
|
69
|
+
function resolveExaDescription(result) {
|
|
70
|
+
const highlights = result.highlights;
|
|
71
|
+
if (Array.isArray(highlights)) {
|
|
72
|
+
const highlightText = highlights.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry)).join("\n");
|
|
73
|
+
if (highlightText) return highlightText;
|
|
74
|
+
}
|
|
75
|
+
const summary = normalizeOptionalString(result.summary);
|
|
76
|
+
if (summary) return summary;
|
|
77
|
+
return normalizeOptionalString(result.text) ?? "";
|
|
78
|
+
}
|
|
79
|
+
function parsePositiveInteger(value) {
|
|
80
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
81
|
+
}
|
|
82
|
+
function invalidContentsPayload(message) {
|
|
83
|
+
return {
|
|
84
|
+
error: "invalid_contents",
|
|
85
|
+
message,
|
|
86
|
+
docs: "https://docs.openclaw.ai/tools/web"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function isErrorPayload(value) {
|
|
90
|
+
return Boolean(value && typeof value === "object" && "error" in value && "message" in value && "docs" in value);
|
|
91
|
+
}
|
|
92
|
+
function resolveExaSearchCount(value, fallback) {
|
|
93
|
+
const parsed = parseStrictPositiveInteger(value);
|
|
94
|
+
if (parsed === void 0) return fallback;
|
|
95
|
+
return Math.min(EXA_MAX_SEARCH_COUNT, parsed);
|
|
96
|
+
}
|
|
97
|
+
function parseExaContents(rawContents) {
|
|
98
|
+
if (rawContents === void 0) return { value: void 0 };
|
|
99
|
+
if (!rawContents || typeof rawContents !== "object" || Array.isArray(rawContents)) return invalidContentsPayload("contents must be an object with optional text, highlights, and summary fields.");
|
|
100
|
+
const raw = rawContents;
|
|
101
|
+
const allowedKeys = new Set([
|
|
102
|
+
"text",
|
|
103
|
+
"highlights",
|
|
104
|
+
"summary"
|
|
105
|
+
]);
|
|
106
|
+
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.`);
|
|
107
|
+
const parsed = {};
|
|
108
|
+
const parseText = (value) => {
|
|
109
|
+
if (typeof value === "boolean") return value;
|
|
110
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalidContentsPayload("contents.text must be a boolean or an object.");
|
|
111
|
+
const obj = value;
|
|
112
|
+
for (const key of Object.keys(obj)) if (key !== "maxCharacters") return invalidContentsPayload(`contents.text has unknown field "${key}". Only "maxCharacters" is allowed.`);
|
|
113
|
+
if ("maxCharacters" in obj && parsePositiveInteger(obj.maxCharacters) === void 0) return invalidContentsPayload("contents.text.maxCharacters must be a positive integer.");
|
|
114
|
+
return parsePositiveInteger(obj.maxCharacters) ? { maxCharacters: parsePositiveInteger(obj.maxCharacters) } : {};
|
|
115
|
+
};
|
|
116
|
+
const parseHighlights = (value) => {
|
|
117
|
+
if (typeof value === "boolean") return value;
|
|
118
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalidContentsPayload("contents.highlights must be a boolean or an object.");
|
|
119
|
+
const obj = value;
|
|
120
|
+
const allowed = new Set([
|
|
121
|
+
"maxCharacters",
|
|
122
|
+
"query",
|
|
123
|
+
"numSentences",
|
|
124
|
+
"highlightsPerUrl"
|
|
125
|
+
]);
|
|
126
|
+
for (const key of Object.keys(obj)) if (!allowed.has(key)) return invalidContentsPayload(`contents.highlights has unknown field "${key}". Allowed fields are "maxCharacters", "query", "numSentences", and "highlightsPerUrl".`);
|
|
127
|
+
if ("maxCharacters" in obj && parsePositiveInteger(obj.maxCharacters) === void 0) return invalidContentsPayload("contents.highlights.maxCharacters must be a positive integer.");
|
|
128
|
+
if ("numSentences" in obj && parsePositiveInteger(obj.numSentences) === void 0) return invalidContentsPayload("contents.highlights.numSentences must be a positive integer.");
|
|
129
|
+
if ("highlightsPerUrl" in obj && parsePositiveInteger(obj.highlightsPerUrl) === void 0) return invalidContentsPayload("contents.highlights.highlightsPerUrl must be a positive integer.");
|
|
130
|
+
if ("query" in obj && typeof obj.query !== "string") return invalidContentsPayload("contents.highlights.query must be a string.");
|
|
131
|
+
return {
|
|
132
|
+
...parsePositiveInteger(obj.maxCharacters) ? { maxCharacters: parsePositiveInteger(obj.maxCharacters) } : {},
|
|
133
|
+
...typeof obj.query === "string" ? { query: obj.query } : {},
|
|
134
|
+
...parsePositiveInteger(obj.numSentences) ? { numSentences: parsePositiveInteger(obj.numSentences) } : {},
|
|
135
|
+
...parsePositiveInteger(obj.highlightsPerUrl) ? { highlightsPerUrl: parsePositiveInteger(obj.highlightsPerUrl) } : {}
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
const parseSummary = (value) => {
|
|
139
|
+
if (typeof value === "boolean") return value;
|
|
140
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return invalidContentsPayload("contents.summary must be a boolean or an object.");
|
|
141
|
+
const obj = value;
|
|
142
|
+
for (const key of Object.keys(obj)) if (key !== "query") return invalidContentsPayload(`contents.summary has unknown field "${key}". Only "query" is allowed.`);
|
|
143
|
+
if ("query" in obj && typeof obj.query !== "string") return invalidContentsPayload("contents.summary.query must be a string.");
|
|
144
|
+
return typeof obj.query === "string" ? { query: obj.query } : {};
|
|
145
|
+
};
|
|
146
|
+
if ("text" in raw) {
|
|
147
|
+
const parsedText = parseText(raw.text);
|
|
148
|
+
if (isErrorPayload(parsedText)) return parsedText;
|
|
149
|
+
parsed.text = parsedText;
|
|
150
|
+
}
|
|
151
|
+
if ("highlights" in raw) {
|
|
152
|
+
const parsedHighlights = parseHighlights(raw.highlights);
|
|
153
|
+
if (isErrorPayload(parsedHighlights)) return parsedHighlights;
|
|
154
|
+
parsed.highlights = parsedHighlights;
|
|
155
|
+
}
|
|
156
|
+
if ("summary" in raw) {
|
|
157
|
+
const parsedSummary = parseSummary(raw.summary);
|
|
158
|
+
if (isErrorPayload(parsedSummary)) return parsedSummary;
|
|
159
|
+
parsed.summary = parsedSummary;
|
|
160
|
+
}
|
|
161
|
+
return { value: parsed };
|
|
162
|
+
}
|
|
163
|
+
function normalizeExaResults(payload) {
|
|
164
|
+
if (!payload || typeof payload !== "object") return [];
|
|
165
|
+
const results = payload.results;
|
|
166
|
+
if (!Array.isArray(results)) return [];
|
|
167
|
+
return results.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
|
|
168
|
+
}
|
|
169
|
+
function resolveFreshnessStartDate(freshness) {
|
|
170
|
+
const now = /* @__PURE__ */ new Date();
|
|
171
|
+
if (freshness === "day") {
|
|
172
|
+
now.setUTCDate(now.getUTCDate() - 1);
|
|
173
|
+
return now.toISOString();
|
|
174
|
+
}
|
|
175
|
+
if (freshness === "week") {
|
|
176
|
+
now.setUTCDate(now.getUTCDate() - 7);
|
|
177
|
+
return now.toISOString();
|
|
178
|
+
}
|
|
179
|
+
if (freshness === "month") {
|
|
180
|
+
const currentDay = now.getUTCDate();
|
|
181
|
+
now.setUTCDate(1);
|
|
182
|
+
now.setUTCMonth(now.getUTCMonth() - 1);
|
|
183
|
+
const lastDayOfTargetMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
|
|
184
|
+
now.setUTCDate(Math.min(currentDay, lastDayOfTargetMonth));
|
|
185
|
+
return now.toISOString();
|
|
186
|
+
}
|
|
187
|
+
now.setUTCFullYear(now.getUTCFullYear() - 1);
|
|
188
|
+
return now.toISOString();
|
|
189
|
+
}
|
|
190
|
+
async function runExaSearch(params) {
|
|
191
|
+
const body = {
|
|
192
|
+
query: params.query,
|
|
193
|
+
numResults: params.count,
|
|
194
|
+
type: params.type,
|
|
195
|
+
contents: params.contents ?? { highlights: true }
|
|
196
|
+
};
|
|
197
|
+
if (params.dateAfter) body.startPublishedDate = params.dateAfter;
|
|
198
|
+
else if (params.freshness) body.startPublishedDate = resolveFreshnessStartDate(params.freshness);
|
|
199
|
+
if (params.dateBefore) body.endPublishedDate = params.dateBefore;
|
|
200
|
+
return withTrustedWebSearchEndpoint({
|
|
201
|
+
url: params.endpoint,
|
|
202
|
+
timeoutSeconds: params.timeoutSeconds,
|
|
203
|
+
init: {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
Accept: "application/json",
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
"x-api-key": params.apiKey,
|
|
209
|
+
"x-exa-integration": "openclaw"
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify(body)
|
|
212
|
+
}
|
|
213
|
+
}, async (res) => {
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
const detail = await readExaErrorDetail(res);
|
|
216
|
+
throw new Error(`Exa API error (${res.status}): ${detail || res.statusText}`);
|
|
217
|
+
}
|
|
218
|
+
return readExaSearchResults(res);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function missingExaKeyPayload() {
|
|
222
|
+
return {
|
|
223
|
+
error: "missing_exa_api_key",
|
|
224
|
+
message: "web_search (exa) needs an Exa API key. Set EXA_API_KEY in the Gateway environment, or configure tools.web.search.exa.apiKey.",
|
|
225
|
+
docs: "https://docs.openclaw.ai/tools/web"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function buildExaCacheKey(params) {
|
|
229
|
+
return buildSearchCacheKey([
|
|
230
|
+
"exa",
|
|
231
|
+
params.endpoint,
|
|
232
|
+
params.type,
|
|
233
|
+
params.query,
|
|
234
|
+
params.count,
|
|
235
|
+
params.freshness,
|
|
236
|
+
params.dateAfter,
|
|
237
|
+
params.dateBefore,
|
|
238
|
+
params.contents?.highlights ? JSON.stringify(params.contents.highlights) : void 0,
|
|
239
|
+
params.contents?.text ? JSON.stringify(params.contents.text) : void 0,
|
|
240
|
+
params.contents?.summary ? JSON.stringify(params.contents.summary) : void 0
|
|
241
|
+
]);
|
|
242
|
+
}
|
|
243
|
+
async function executeExaWebSearchProviderTool(ctx, args) {
|
|
244
|
+
const searchConfig = mergeScopedSearchConfig(ctx.searchConfig, "exa", resolveProviderWebSearchPluginConfig(ctx.config, "exa"));
|
|
245
|
+
const params = args;
|
|
246
|
+
const exaConfig = resolveExaConfig(searchConfig);
|
|
247
|
+
const apiKey = resolveExaApiKey(exaConfig);
|
|
248
|
+
if (!apiKey) return missingExaKeyPayload();
|
|
249
|
+
const endpointResult = resolveExaSearchEndpoint(exaConfig);
|
|
250
|
+
if ("error" in endpointResult) return endpointResult;
|
|
251
|
+
const endpoint = endpointResult.endpoint;
|
|
252
|
+
const query = readStringParam(params, "query", { required: true });
|
|
253
|
+
const rawType = readStringParam(params, "type");
|
|
254
|
+
const type = EXA_SEARCH_TYPES.includes(rawType) ? rawType : "auto";
|
|
255
|
+
const count = readPositiveIntegerParam(params, "count", {
|
|
256
|
+
max: EXA_MAX_SEARCH_COUNT,
|
|
257
|
+
message: `count must be an integer from 1 to ${EXA_MAX_SEARCH_COUNT}.`
|
|
258
|
+
}) ?? searchConfig?.maxResults ?? void 0;
|
|
259
|
+
const rawFreshness = readStringParam(params, "freshness");
|
|
260
|
+
const freshness = normalizeExaFreshness(rawFreshness);
|
|
261
|
+
if (rawFreshness && !freshness) return {
|
|
262
|
+
error: "invalid_freshness",
|
|
263
|
+
message: "freshness must be one of \"day\", \"week\", \"month\", or \"year\".",
|
|
264
|
+
docs: "https://docs.openclaw.ai/tools/web"
|
|
265
|
+
};
|
|
266
|
+
const rawDateAfter = readStringParam(params, "date_after");
|
|
267
|
+
const rawDateBefore = readStringParam(params, "date_before");
|
|
268
|
+
if (freshness && (rawDateAfter || rawDateBefore)) return {
|
|
269
|
+
error: "conflicting_time_filters",
|
|
270
|
+
message: "freshness cannot be combined with date_after or date_before. Use one time-filter mode.",
|
|
271
|
+
docs: "https://docs.openclaw.ai/tools/web"
|
|
272
|
+
};
|
|
273
|
+
const parsedDateRange = parseIsoDateRange({
|
|
274
|
+
rawDateAfter,
|
|
275
|
+
rawDateBefore,
|
|
276
|
+
invalidDateAfterMessage: "date_after must be YYYY-MM-DD format.",
|
|
277
|
+
invalidDateBeforeMessage: "date_before must be YYYY-MM-DD format.",
|
|
278
|
+
invalidDateRangeMessage: "date_after must be earlier than or equal to date_before."
|
|
279
|
+
});
|
|
280
|
+
if ("error" in parsedDateRange) return parsedDateRange;
|
|
281
|
+
const { dateAfter, dateBefore } = parsedDateRange;
|
|
282
|
+
const parsedContents = parseExaContents(params.contents);
|
|
283
|
+
if (isErrorPayload(parsedContents)) return parsedContents;
|
|
284
|
+
const contents = parsedContents.value && Object.keys(parsedContents.value).length > 0 ? parsedContents.value : void 0;
|
|
285
|
+
const resolvedCount = resolveExaSearchCount(count, DEFAULT_SEARCH_COUNT);
|
|
286
|
+
const cacheKey = buildExaCacheKey({
|
|
287
|
+
endpoint,
|
|
288
|
+
type,
|
|
289
|
+
query,
|
|
290
|
+
count: resolvedCount,
|
|
291
|
+
freshness,
|
|
292
|
+
dateAfter,
|
|
293
|
+
dateBefore,
|
|
294
|
+
contents
|
|
295
|
+
});
|
|
296
|
+
const cached = readCachedSearchPayload(cacheKey);
|
|
297
|
+
if (cached) return cached;
|
|
298
|
+
const start = Date.now();
|
|
299
|
+
const results = await runExaSearch({
|
|
300
|
+
apiKey,
|
|
301
|
+
endpoint,
|
|
302
|
+
query,
|
|
303
|
+
count: resolvedCount,
|
|
304
|
+
freshness,
|
|
305
|
+
dateAfter,
|
|
306
|
+
dateBefore,
|
|
307
|
+
type,
|
|
308
|
+
contents,
|
|
309
|
+
timeoutSeconds: resolveSearchTimeoutSeconds(searchConfig)
|
|
310
|
+
});
|
|
311
|
+
const payload = {
|
|
312
|
+
query,
|
|
313
|
+
provider: "exa",
|
|
314
|
+
count: results.length,
|
|
315
|
+
tookMs: Date.now() - start,
|
|
316
|
+
externalContent: {
|
|
317
|
+
untrusted: true,
|
|
318
|
+
source: "web_search",
|
|
319
|
+
provider: "exa",
|
|
320
|
+
wrapped: true
|
|
321
|
+
},
|
|
322
|
+
results: results.map((entry) => {
|
|
323
|
+
const title = typeof entry.title === "string" ? entry.title : "";
|
|
324
|
+
const url = typeof entry.url === "string" ? entry.url : "";
|
|
325
|
+
const description = resolveExaDescription(entry);
|
|
326
|
+
const summary = normalizeOptionalString(entry.summary) ?? "";
|
|
327
|
+
const highlightScores = Array.isArray(entry.highlightScores) ? entry.highlightScores.filter((score) => typeof score === "number" && Number.isFinite(score)) : [];
|
|
328
|
+
const published = typeof entry.publishedDate === "string" && entry.publishedDate ? entry.publishedDate : void 0;
|
|
329
|
+
return Object.assign({
|
|
330
|
+
title: title ? wrapWebContent(title, `web_search`) : ``,
|
|
331
|
+
url,
|
|
332
|
+
description: description ? wrapWebContent(description, `web_search`) : ``,
|
|
333
|
+
published,
|
|
334
|
+
siteName: resolveSiteName(url) || void 0
|
|
335
|
+
}, summary ? { summary: wrapWebContent(summary, `web_search`) } : {}, highlightScores.length > 0 ? { highlightScores } : {});
|
|
336
|
+
})
|
|
337
|
+
};
|
|
338
|
+
writeCachedSearchPayload(cacheKey, payload, resolveSearchCacheTtlMs(searchConfig));
|
|
339
|
+
return payload;
|
|
340
|
+
}
|
|
341
|
+
const testing = {
|
|
342
|
+
normalizeExaResults,
|
|
343
|
+
normalizeExaFreshness,
|
|
344
|
+
parseExaContents,
|
|
345
|
+
buildExaCacheKey,
|
|
346
|
+
resolveExaApiKey,
|
|
347
|
+
resolveExaConfig,
|
|
348
|
+
resolveExaDescription,
|
|
349
|
+
resolveExaSearchCount,
|
|
350
|
+
resolveExaSearchEndpoint,
|
|
351
|
+
resolveFreshnessStartDate,
|
|
352
|
+
readExaErrorDetail,
|
|
353
|
+
readExaSearchResults
|
|
354
|
+
};
|
|
355
|
+
//#endregion
|
|
356
|
+
export { testing as __testing, testing, executeExaWebSearchProviderTool };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createWebSearchProviderContractFields } from "openclaw/plugin-sdk/provider-web-search-contract";
|
|
2
|
+
//#region extensions/exa/src/exa-web-search-provider.shared.ts
|
|
3
|
+
const EXA_CREDENTIAL_PATH = "plugins.entries.exa.config.webSearch.apiKey";
|
|
4
|
+
const EXA_ONBOARDING_SCOPES = ["text-inference"];
|
|
5
|
+
function createExaWebSearchProviderBase() {
|
|
6
|
+
return {
|
|
7
|
+
id: "exa",
|
|
8
|
+
label: "Exa Search",
|
|
9
|
+
hint: "Neural + keyword search with date filters and content extraction",
|
|
10
|
+
onboardingScopes: [...EXA_ONBOARDING_SCOPES],
|
|
11
|
+
credentialLabel: "Exa API key",
|
|
12
|
+
envVars: ["EXA_API_KEY"],
|
|
13
|
+
placeholder: "exa-...",
|
|
14
|
+
signupUrl: "https://exa.ai/",
|
|
15
|
+
docsUrl: "https://docs.openclaw.ai/tools/web",
|
|
16
|
+
autoDetectOrder: 65,
|
|
17
|
+
credentialPath: EXA_CREDENTIAL_PATH,
|
|
18
|
+
...createWebSearchProviderContractFields({
|
|
19
|
+
credentialPath: EXA_CREDENTIAL_PATH,
|
|
20
|
+
searchCredential: {
|
|
21
|
+
type: "scoped",
|
|
22
|
+
scopeId: "exa"
|
|
23
|
+
},
|
|
24
|
+
configuredCredential: { pluginId: "exa" },
|
|
25
|
+
selectionPluginId: "exa"
|
|
26
|
+
})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { createExaWebSearchProviderBase as t };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as createExaWebSearchProvider } from "./exa-web-search-provider-q-LG8WsD.js";
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
//#region extensions/exa/index.ts
|
|
4
|
+
var exa_default = definePluginEntry({
|
|
5
|
+
id: "exa",
|
|
6
|
+
name: "Exa Plugin",
|
|
7
|
+
description: "Bundled Exa web search plugin",
|
|
8
|
+
register(api) {
|
|
9
|
+
api.registerWebSearchProvider(createExaWebSearchProvider());
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { exa_default as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t as createExaWebSearchProviderBase } from "./exa-web-search-provider.shared-CWlikD_3.js";
|
|
2
|
+
//#region extensions/exa/web-search-contract-api.ts
|
|
3
|
+
function createExaWebSearchProvider() {
|
|
4
|
+
return {
|
|
5
|
+
...createExaWebSearchProviderBase(),
|
|
6
|
+
createTool: () => null
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { createExaWebSearchProvider };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "exa",
|
|
3
|
+
"activation": {
|
|
4
|
+
"onStartup": false
|
|
5
|
+
},
|
|
6
|
+
"setup": {
|
|
7
|
+
"providers": [
|
|
8
|
+
{
|
|
9
|
+
"id": "exa",
|
|
10
|
+
"envVars": ["EXA_API_KEY"]
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"uiHints": {
|
|
15
|
+
"webSearch.apiKey": {
|
|
16
|
+
"label": "Exa API Key",
|
|
17
|
+
"help": "Exa Search API key (fallback: EXA_API_KEY env var).",
|
|
18
|
+
"sensitive": true,
|
|
19
|
+
"placeholder": "exa-..."
|
|
20
|
+
},
|
|
21
|
+
"webSearch.baseUrl": {
|
|
22
|
+
"label": "Exa Search Base URL",
|
|
23
|
+
"help": "Optional Exa Search API base URL override. OpenClaw appends /search when the URL does not already end there."
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"contracts": {
|
|
27
|
+
"webSearchProviders": ["exa"]
|
|
28
|
+
},
|
|
29
|
+
"configContracts": {
|
|
30
|
+
"compatibilityRuntimePaths": ["tools.web.search.apiKey"]
|
|
31
|
+
},
|
|
32
|
+
"configSchema": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"properties": {
|
|
36
|
+
"webSearch": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"additionalProperties": false,
|
|
39
|
+
"properties": {
|
|
40
|
+
"apiKey": {
|
|
41
|
+
"type": ["string", "object"]
|
|
42
|
+
},
|
|
43
|
+
"baseUrl": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/exa-plugin",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "2026.6.9",
|
|
4
|
+
"description": "OpenClaw Exa plugin.",
|
|
6
5
|
"repository": {
|
|
7
6
|
"type": "git",
|
|
8
|
-
"url": "
|
|
7
|
+
"url": "https://github.com/openclaw/openclaw"
|
|
9
8
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
9
|
+
"type": "module",
|
|
10
|
+
"openclaw": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./index.ts"
|
|
13
|
+
],
|
|
14
|
+
"install": {
|
|
15
|
+
"clawhubSpec": "clawhub:@openclaw/exa-plugin",
|
|
16
|
+
"npmSpec": "@openclaw/exa-plugin",
|
|
17
|
+
"defaultChoice": "npm",
|
|
18
|
+
"minHostVersion": ">=2026.6.8"
|
|
19
|
+
},
|
|
20
|
+
"compat": {
|
|
21
|
+
"pluginApi": ">=2026.6.9"
|
|
22
|
+
},
|
|
23
|
+
"build": {
|
|
24
|
+
"openclawVersion": "2026.6.9",
|
|
25
|
+
"bundledDist": false
|
|
26
|
+
},
|
|
27
|
+
"release": {
|
|
28
|
+
"publishToClawHub": true,
|
|
29
|
+
"publishToNpm": true
|
|
30
|
+
},
|
|
31
|
+
"runtimeExtensions": [
|
|
32
|
+
"./dist/index.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/**",
|
|
37
|
+
"openclaw.plugin.json",
|
|
38
|
+
"npm-shrinkwrap.json",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"openclaw": ">=2026.6.9"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"openclaw": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"bundledDependencies": []
|
|
14
50
|
}
|