@tonydua/dsh-web-search-exa 0.1.5 → 0.2.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.
package/lib/index.d.ts CHANGED
@@ -67,6 +67,8 @@ interface ExaMcpSection {
67
67
  }
68
68
  //#endregion
69
69
  //#region src/provider.d.ts
70
+ /** MCP tool names the anonymous path can call. */
71
+ type ExaMcpTool = 'web_search_exa' | 'web_search_advanced_exa';
70
72
  /**
71
73
  * Fully resolved options the provider serves one search with. Produced by
72
74
  * {@link resolveOptions} from the current Settings section, so every field is
@@ -83,6 +85,7 @@ interface ExaSearchProviderOptions {
83
85
  readonly baseURL: string;
84
86
  readonly apiURL?: string | undefined;
85
87
  readonly mcpURL: string;
88
+ readonly mcpTool: ExaMcpTool;
86
89
  readonly searchType: ExaSearchType;
87
90
  readonly numResults?: number | undefined;
88
91
  readonly highlightsPerResult: number;
@@ -96,6 +99,7 @@ interface ExaSearchProviderConfig {
96
99
  /** @deprecated Use `baseURL`; this full endpoint remains supported for compatibility. */
97
100
  apiURL?: string;
98
101
  mcpURL?: string;
102
+ mcpTool?: ExaMcpTool;
99
103
  searchType?: ExaSearchType;
100
104
  numResults?: number;
101
105
  highlightsPerResult?: number;
@@ -154,6 +158,18 @@ export declare class ExaTransientError extends WebError {
154
158
  export declare class ExaRateLimitError extends WebError {
155
159
  constructor(message: string);
156
160
  }
161
+ /**
162
+ * An anonymous response body that exceeded {@link MAX_MCP_RESPONSE_BYTES}.
163
+ *
164
+ * Extends {@link ExaTransientError} because the endpoint, not the caller's
165
+ * configuration, produced it: a body this size is a bad day on Exa's side, and
166
+ * the breaker should get to count it.
167
+ */
168
+ export declare class ExaResponseTooLargeError extends ExaTransientError {
169
+ /** The number of bytes observed, or `undefined` when the server declared the size. */
170
+ readonly observedBytes: number | undefined;
171
+ constructor(limit: number, observedBytes?: number);
172
+ }
157
173
  /**
158
174
  * Consecutive-transient-failure breaker for the keyless path.
159
175
  *
@@ -242,16 +258,37 @@ export declare const PROVIDER_ID = "exa";
242
258
  export declare const DEFAULT_BASE_URL = "https://api.exa.ai";
243
259
  /** Legacy full REST endpoint; `baseURL` is the canonical dsh-compatible option. */
244
260
  export declare const DEFAULT_API_URL = "https://api.exa.ai/search";
245
- /** Exa hosted MCP endpoint; the anonymous fallback path. */
246
- export declare const DEFAULT_MCP_URL = "https://mcp.exa.ai/mcp";
261
+ /**
262
+ * Exa hosted MCP endpoint; the anonymous fallback path.
263
+ *
264
+ * The `tools` query is part of the default because `web_search_advanced_exa`
265
+ * is not servable without it — a request naming it against the bare endpoint
266
+ * fails with `MCP error -32602: Tool web_search_advanced_exa not found`.
267
+ * A configured `mcpURL` that omits `tools` gets the query spliced in at request
268
+ * time, so existing configurations keep working.
269
+ */
270
+ export declare const DEFAULT_MCP_URL = "https://mcp.exa.ai/mcp?tools=web_search_exa,web_search_advanced_exa";
247
271
  /** Environment variable consulted when no literal `apiKey` is configured. */
248
272
  export declare const DEFAULT_API_KEY_ENV = "EXA_API_KEY";
249
273
  /** Default retrieval mode for the REST path: let Exa pick. */
250
274
  export declare const DEFAULT_SEARCH_TYPE = "auto";
251
275
  /** Default number of highlight sentences requested per result (REST path). */
252
276
  export declare const DEFAULT_HIGHLIGHTS_PER_RESULT = 1;
253
- /** MCP tool name for plain web search on Exa's hosted server. */
277
+ /** MCP tool name for plain web search on Exa's hosted server (text-blob output). */
254
278
  export declare const MCP_TOOL = "web_search_exa";
279
+ /** MCP tool whose text content is a sanitized structured search response. */
280
+ export declare const MCP_TOOL_ADVANCED = "web_search_advanced_exa";
281
+ /** The tool the anonymous path calls by default. */
282
+ export declare const DEFAULT_MCP_TOOL = "web_search_advanced_exa";
283
+ /** Query parameter enabling both MCP tools when a configured URL omits it. */
284
+ export declare const MCP_TOOLS_QUERY = "tools=web_search_exa,web_search_advanced_exa";
285
+ /**
286
+ * Reject anonymous MCP responses larger than this.
287
+ *
288
+ * Structured results are kilobytes; anything past this is a malformed or
289
+ * hostile body and parsing it would only burn memory before failing anyway.
290
+ */
291
+ export declare const MAX_MCP_RESPONSE_BYTES: number;
255
292
  /**
256
293
  * Attribution header sent on anonymous MCP requests. This is the only signal
257
294
  * Exa's public endpoint receives about the caller, so it is deliberately a
@@ -295,6 +332,13 @@ declare const Config: z<Schemastery.ObjectS<{
295
332
  apiURL: z<string, string>;
296
333
  /** Exa hosted MCP endpoint, used by the anonymous fallback. */
297
334
  mcpURL: z<string, string>;
335
+ /**
336
+ * MCP tool the anonymous path calls. `web_search_advanced_exa` returns a
337
+ * sanitized structured JSON response; `web_search_exa` returns the
338
+ * `Title:`-section text blob. The structured tool is the default because it
339
+ * needs no text parsing, and the text path stays available as a fallback.
340
+ */
341
+ mcpTool: z<"web_search_exa" | "web_search_advanced_exa", "web_search_exa" | "web_search_advanced_exa">;
298
342
  /** REST retrieval mode: `auto`, `keyword`, or `neural`. */
299
343
  searchType: z<"auto" | "keyword" | "neural", "auto" | "keyword" | "neural">;
300
344
  /** Default result count when the request carries no `maxResults`. */
@@ -324,6 +368,13 @@ declare const Config: z<Schemastery.ObjectS<{
324
368
  apiURL: z<string, string>;
325
369
  /** Exa hosted MCP endpoint, used by the anonymous fallback. */
326
370
  mcpURL: z<string, string>;
371
+ /**
372
+ * MCP tool the anonymous path calls. `web_search_advanced_exa` returns a
373
+ * sanitized structured JSON response; `web_search_exa` returns the
374
+ * `Title:`-section text blob. The structured tool is the default because it
375
+ * needs no text parsing, and the text path stays available as a fallback.
376
+ */
377
+ mcpTool: z<"web_search_exa" | "web_search_advanced_exa", "web_search_exa" | "web_search_advanced_exa">;
327
378
  /** REST retrieval mode: `auto`, `keyword`, or `neural`. */
328
379
  searchType: z<"auto" | "keyword" | "neural", "auto" | "keyword" | "neural">;
329
380
  /** Default result count when the request carries no `maxResults`. */
@@ -383,4 +434,4 @@ declare function installSettingsSection(settings: SettingsServiceLike, owner: un
383
434
  */
384
435
  export declare function apply(ctx: Context, config: ExaSearchProviderConfig): void;
385
436
  //#endregion
386
- export { Config, type ExaApiKeyResolver, type ExaKeyEnvironment, type ExaMcpSection, type ExaOptionsResolver, type ExaRestResponse, type ExaRestResult, type ExaSearchProviderConfig, type ExaSearchProviderOptions, type ExaSearchType, type McpContentItem, type McpJsonRpcError, type McpPayload, type McpToolResult, installSettingsSection };
437
+ export { Config, type ExaApiKeyResolver, type ExaKeyEnvironment, type ExaMcpSection, type ExaMcpTool, type ExaOptionsResolver, type ExaRestResponse, type ExaRestResult, type ExaSearchProviderConfig, type ExaSearchProviderOptions, type ExaSearchType, type McpContentItem, type McpJsonRpcError, type McpPayload, type McpToolResult, installSettingsSection };
package/lib/index.js CHANGED
@@ -20,16 +20,37 @@ const PROVIDER_ID = "exa";
20
20
  const DEFAULT_BASE_URL = "https://api.exa.ai";
21
21
  /** Legacy full REST endpoint; `baseURL` is the canonical dsh-compatible option. */
22
22
  const DEFAULT_API_URL = `${DEFAULT_BASE_URL}/search`;
23
- /** Exa hosted MCP endpoint; the anonymous fallback path. */
24
- const DEFAULT_MCP_URL = "https://mcp.exa.ai/mcp";
23
+ /**
24
+ * Exa hosted MCP endpoint; the anonymous fallback path.
25
+ *
26
+ * The `tools` query is part of the default because `web_search_advanced_exa`
27
+ * is not servable without it — a request naming it against the bare endpoint
28
+ * fails with `MCP error -32602: Tool web_search_advanced_exa not found`.
29
+ * A configured `mcpURL` that omits `tools` gets the query spliced in at request
30
+ * time, so existing configurations keep working.
31
+ */
32
+ const DEFAULT_MCP_URL = "https://mcp.exa.ai/mcp?tools=web_search_exa,web_search_advanced_exa";
25
33
  /** Environment variable consulted when no literal `apiKey` is configured. */
26
34
  const DEFAULT_API_KEY_ENV = "EXA_API_KEY";
27
35
  /** Default retrieval mode for the REST path: let Exa pick. */
28
36
  const DEFAULT_SEARCH_TYPE = "auto";
29
37
  /** Default number of highlight sentences requested per result (REST path). */
30
38
  const DEFAULT_HIGHLIGHTS_PER_RESULT = 1;
31
- /** MCP tool name for plain web search on Exa's hosted server. */
39
+ /** MCP tool name for plain web search on Exa's hosted server (text-blob output). */
32
40
  const MCP_TOOL = "web_search_exa";
41
+ /** MCP tool whose text content is a sanitized structured search response. */
42
+ const MCP_TOOL_ADVANCED = "web_search_advanced_exa";
43
+ /** The tool the anonymous path calls by default. */
44
+ const DEFAULT_MCP_TOOL = MCP_TOOL_ADVANCED;
45
+ /** Query parameter enabling both MCP tools when a configured URL omits it. */
46
+ const MCP_TOOLS_QUERY = "tools=web_search_exa,web_search_advanced_exa";
47
+ /**
48
+ * Reject anonymous MCP responses larger than this.
49
+ *
50
+ * Structured results are kilobytes; anything past this is a malformed or
51
+ * hostile body and parsing it would only burn memory before failing anyway.
52
+ */
53
+ const MAX_MCP_RESPONSE_BYTES = 262144;
33
54
  /**
34
55
  * Attribution header sent on anonymous MCP requests. This is the only signal
35
56
  * Exa's public endpoint receives about the caller, so it is deliberately a
@@ -110,6 +131,136 @@ function mapRestResult(result) {
110
131
  };
111
132
  }
112
133
  /**
134
+ * Read an anonymous response body while refusing to buffer more than `limit`
135
+ * bytes.
136
+ *
137
+ * The cap has to be enforced *while reading*, not after: `await
138
+ * response.text()` materializes the whole body first, so a size check that
139
+ * follows it protects nothing — the memory has already been spent, and
140
+ * `new TextEncoder().encode(text)` spends a second copy of it just to measure
141
+ * the first. This helper instead:
142
+ *
143
+ * 1. rejects immediately when the server declares an over-limit
144
+ * `content-length`, so the body is never requested;
145
+ * 2. otherwise reads the stream chunk by chunk, aborting the transfer as soon
146
+ * as the running byte count passes `limit`.
147
+ *
148
+ * The limit is a memory boundary on the keyless path (a shared, unauthenticated
149
+ * endpoint), not a judgement about the result set: the structured tool returns
150
+ * whole-page text for every hit, so a large-but-legitimate response is possible
151
+ * and is reported as a transient failure rather than a silent empty result.
152
+ *
153
+ * @param response - the ok response whose body is to be read.
154
+ * @param limit - the maximum number of bytes to buffer.
155
+ * @returns the decoded body text.
156
+ * @throws {ExaResponseTooLargeError} when the body is, or grows, past `limit`.
157
+ */
158
+ async function readBoundedBody(response, limit) {
159
+ const declared = response.headers.get("content-length");
160
+ if (declared !== null) {
161
+ const declaredBytes = Number(declared);
162
+ if (Number.isFinite(declaredBytes) && declaredBytes > limit) {
163
+ await response.body?.cancel().catch(() => {});
164
+ throw new ExaResponseTooLargeError(limit, declaredBytes);
165
+ }
166
+ }
167
+ const body = response.body;
168
+ if (body === null) return await response.text();
169
+ const reader = body.getReader();
170
+ const chunks = [];
171
+ let received = 0;
172
+ try {
173
+ for (;;) {
174
+ const { done, value } = await reader.read();
175
+ if (done) break;
176
+ received += value.byteLength;
177
+ if (received > limit) {
178
+ await reader.cancel().catch(() => {});
179
+ throw new ExaResponseTooLargeError(limit, received);
180
+ }
181
+ chunks.push(value);
182
+ }
183
+ } finally {
184
+ reader.releaseLock();
185
+ }
186
+ const merged = new Uint8Array(received);
187
+ let offset = 0;
188
+ for (const chunk of chunks) {
189
+ merged.set(chunk, offset);
190
+ offset += chunk.byteLength;
191
+ }
192
+ return new TextDecoder().decode(merged);
193
+ }
194
+ /**
195
+ * Return the request URL for an anonymous advanced-tool search: when the
196
+ * configured MCP URL carries no `tools` parameter, splice in the query that
197
+ * enables both tools — the advanced tool is not servable otherwise. An existing
198
+ * query string is preserved.
199
+ *
200
+ * @param baseURL - the configured MCP endpoint.
201
+ * @returns the request URL with a non-empty `tools` query present.
202
+ */
203
+ function endpointFor(baseURL) {
204
+ const queryIndex = baseURL.indexOf("?");
205
+ if (queryIndex >= 0) {
206
+ const tools = new URLSearchParams(baseURL.slice(queryIndex + 1)).get("tools");
207
+ if (tools != null && tools.length > 0) return baseURL;
208
+ return `${baseURL}&${MCP_TOOLS_QUERY}`;
209
+ }
210
+ return `${baseURL}?${MCP_TOOLS_QUERY}`;
211
+ }
212
+ /**
213
+ * Map one sanitized advanced-tool result to a normalized source, or `undefined`
214
+ * when it has no portable snippet. The advanced tool returns the REST result
215
+ * vocabulary as JSON, so this mirrors {@link mapRestResult} — including its rule
216
+ * that a snippet must be a real highlight, never the long-form `text` field,
217
+ * because a fabricated snippet would make the seam lie.
218
+ *
219
+ * @param result - one structured entry from the sanitized response.
220
+ * @returns a normalized source, or `undefined` when the entry is unusable.
221
+ */
222
+ function mapAdvancedResult(result) {
223
+ if (typeof result !== "object" || result === null) return void 0;
224
+ const entry = result;
225
+ if (typeof entry.url !== "string" || entry.url.length === 0) return void 0;
226
+ const snippet = (Array.isArray(entry.highlights) ? entry.highlights : void 0)?.find((highlight) => typeof highlight === "string" && highlight.trim().length > 0);
227
+ if (snippet === void 0) return void 0;
228
+ const title = typeof entry.title === "string" ? entry.title : void 0;
229
+ const publishedDate = typeof entry.publishedDate === "string" ? entry.publishedDate : void 0;
230
+ return {
231
+ url: entry.url,
232
+ ...title != null && title.length > 0 ? { title } : {},
233
+ snippet,
234
+ ...publishedDate != null && publishedDate.length > 0 ? { publishedAt: publishedDate } : {}
235
+ };
236
+ }
237
+ /**
238
+ * Extract sources from a successful advanced-tool payload: the first text item
239
+ * is the sanitized search response JSON, in the REST envelope shape
240
+ * (`{ results: [...] }`).
241
+ *
242
+ * @param payload - the parsed JSON-RPC payload.
243
+ * @returns the normalized sources, or `null` when the body is not the expected
244
+ * shape — the caller then falls back to `Title:`-section parsing, so a future
245
+ * change to the tool's output degrades instead of breaking.
246
+ */
247
+ function parseAdvancedPayload(payload) {
248
+ const content = payload?.result?.content;
249
+ if (!Array.isArray(content)) return null;
250
+ const text = content.find((item) => typeof item?.text === "string")?.text;
251
+ if (text === void 0) return null;
252
+ let parsed;
253
+ try {
254
+ parsed = JSON.parse(text);
255
+ } catch {
256
+ return null;
257
+ }
258
+ if (typeof parsed !== "object" || parsed === null) return null;
259
+ if (parsed.results === void 0) return [];
260
+ if (!Array.isArray(parsed.results)) return null;
261
+ return parsed.results.map(mapAdvancedResult).filter((source) => source !== void 0);
262
+ }
263
+ /**
113
264
  * Parse an SSE (`text/event-stream`) response body into its first `data:`
114
265
  * payload, falling back to plain JSON. Returns `null` when neither parses.
115
266
  */
@@ -235,6 +386,22 @@ var ExaRateLimitError = class extends WebError {
235
386
  }
236
387
  };
237
388
  /**
389
+ * An anonymous response body that exceeded {@link MAX_MCP_RESPONSE_BYTES}.
390
+ *
391
+ * Extends {@link ExaTransientError} because the endpoint, not the caller's
392
+ * configuration, produced it: a body this size is a bad day on Exa's side, and
393
+ * the breaker should get to count it.
394
+ */
395
+ var ExaResponseTooLargeError = class extends ExaTransientError {
396
+ /** The number of bytes observed, or `undefined` when the server declared the size. */
397
+ observedBytes;
398
+ constructor(limit, observedBytes) {
399
+ super(`Exa anonymous MCP response exceeded ${limit} bytes` + (observedBytes === void 0 ? "" : ` (received ${observedBytes})`));
400
+ this.name = "ExaResponseTooLargeError";
401
+ this.observedBytes = observedBytes;
402
+ }
403
+ };
404
+ /**
238
405
  * Consecutive-transient-failure breaker for the keyless path.
239
406
  *
240
407
  * The public MCP endpoint is best-effort: when it is throttling or down, every
@@ -291,7 +458,8 @@ function resolveOptions(section) {
291
458
  apiKeyEnv: section.apiKeyEnv ?? "EXA_API_KEY",
292
459
  baseURL,
293
460
  apiURL: section.apiURL ?? `${baseURL.replace(/\/+$/, "")}/search`,
294
- mcpURL: section.mcpURL ?? "https://mcp.exa.ai/mcp",
461
+ mcpURL: section.mcpURL ?? "https://mcp.exa.ai/mcp?tools=web_search_exa,web_search_advanced_exa",
462
+ mcpTool: section.mcpTool ?? "web_search_advanced_exa",
295
463
  searchType: section.searchType ?? "auto",
296
464
  numResults: section.numResults,
297
465
  highlightsPerResult: section.highlightsPerResult ?? 1
@@ -424,12 +592,18 @@ var ExaSearchProvider = class {
424
592
  */
425
593
  async #anonymousMcpSearch(request, options, signal) {
426
594
  throwIfAborted(signal);
595
+ const tool = options.mcpTool;
596
+ const isAdvanced = tool === "web_search_advanced_exa";
427
597
  const args = { query: request.query };
428
598
  const numResults = request.maxResults ?? options.numResults;
429
599
  if (numResults !== void 0) args.numResults = numResults;
600
+ if (isAdvanced) {
601
+ args.enableHighlights = true;
602
+ args.highlightsNumSentences = options.highlightsPerResult ?? 1;
603
+ }
430
604
  let response;
431
605
  try {
432
- response = await fetch(options.mcpURL, {
606
+ response = await fetch(isAdvanced ? endpointFor(options.mcpURL) : options.mcpURL, {
433
607
  method: "POST",
434
608
  redirect: "error",
435
609
  headers: {
@@ -442,7 +616,7 @@ var ExaSearchProvider = class {
442
616
  id: Math.random().toString(36).slice(2),
443
617
  method: "tools/call",
444
618
  params: {
445
- name: MCP_TOOL,
619
+ name: tool,
446
620
  arguments: args
447
621
  }
448
622
  }),
@@ -457,19 +631,26 @@ var ExaSearchProvider = class {
457
631
  if (isTransientStatus(response.status)) throw new ExaTransientError(`Exa anonymous MCP error (HTTP ${response.status})`);
458
632
  throw new WebError(`Exa anonymous MCP error (HTTP ${response.status})`, "WEB_PROVIDER_ERROR");
459
633
  }
460
- let payload;
634
+ let text;
461
635
  try {
462
- payload = parseSsePayload(await response.text());
636
+ text = await readBoundedBody(response, MAX_MCP_RESPONSE_BYTES);
463
637
  } catch (error) {
464
638
  if (signal?.aborted === true || isAbortError(error)) throw new WebError("Exa anonymous search aborted", "WEB_ABORTED", { cause: signal?.reason ?? error });
639
+ if (error instanceof ExaResponseTooLargeError) throw error;
465
640
  throw new ExaTransientError(`Exa returned an unprocessable response body: ${String(error)}`, error);
466
641
  }
642
+ const payload = parseSsePayload(text);
467
643
  if (payload === null) throw new ExaTransientError("Exa anonymous MCP returned an unprocessable response body");
468
644
  if (payload.error != null) throw new WebError(`Exa MCP error: ${String(payload.error.message ?? JSON.stringify(payload.error))}`, "WEB_PROVIDER_ERROR");
469
645
  if (payload.result?.isError === true) {
470
646
  const detail = collectMcpText(payload).join("\n").trim();
471
647
  throw new WebError(`Exa MCP tool error${detail.length > 0 ? `: ${detail}` : ""}`, "WEB_PROVIDER_ERROR");
472
648
  }
649
+ const structured = isAdvanced ? parseAdvancedPayload(payload) : null;
650
+ if (structured !== null) return {
651
+ sources: structured,
652
+ truncated: false
653
+ };
473
654
  return {
474
655
  sources: mapMcpSections(splitExaSections(collectMcpText(payload).join("\n\n"))),
475
656
  truncated: false
@@ -501,6 +682,13 @@ const Config = z.object({
501
682
  apiURL: z.string(),
502
683
  /** Exa hosted MCP endpoint, used by the anonymous fallback. */
503
684
  mcpURL: z.string().default(DEFAULT_MCP_URL),
685
+ /**
686
+ * MCP tool the anonymous path calls. `web_search_advanced_exa` returns a
687
+ * sanitized structured JSON response; `web_search_exa` returns the
688
+ * `Title:`-section text blob. The structured tool is the default because it
689
+ * needs no text parsing, and the text path stays available as a fallback.
690
+ */
691
+ mcpTool: z.union(["web_search_exa", "web_search_advanced_exa"]).default(DEFAULT_MCP_TOOL),
504
692
  /** REST retrieval mode: `auto`, `keyword`, or `neural`. */
505
693
  searchType: z.union([
506
694
  "auto",
@@ -557,4 +745,4 @@ function apply(ctx, config) {
557
745
  ctx.web.registerSearchProvider(new ExaSearchProvider(() => resolveOptions(current()), (options) => resolveApiKey(options, environment)));
558
746
  }
559
747
  //#endregion
560
- export { Config, DEFAULT_API_KEY_ENV, DEFAULT_API_URL, DEFAULT_BASE_URL, DEFAULT_BREAKER_COOLDOWN_MS, DEFAULT_BREAKER_THRESHOLD, DEFAULT_HIGHLIGHTS_PER_RESULT, DEFAULT_MCP_URL, DEFAULT_PROVIDER_ID, DEFAULT_SEARCH_TYPE, ExaAvailabilityBreaker, ExaRateLimitError, ExaSearchProvider, ExaTransientError, MAX_SNIPPET_CHARS, MCP_SOURCE, MCP_TOOL, PROVIDER_ID, SETTINGS_NAMESPACE, USER_AGENT, apply, inject, installSettingsSection, name, resolveApiKey, resolveApiKeyFromProcess, resolveOptions };
748
+ export { Config, DEFAULT_API_KEY_ENV, DEFAULT_API_URL, DEFAULT_BASE_URL, DEFAULT_BREAKER_COOLDOWN_MS, DEFAULT_BREAKER_THRESHOLD, DEFAULT_HIGHLIGHTS_PER_RESULT, DEFAULT_MCP_TOOL, DEFAULT_MCP_URL, DEFAULT_PROVIDER_ID, DEFAULT_SEARCH_TYPE, ExaAvailabilityBreaker, ExaRateLimitError, ExaResponseTooLargeError, ExaSearchProvider, ExaTransientError, MAX_MCP_RESPONSE_BYTES, MAX_SNIPPET_CHARS, MCP_SOURCE, MCP_TOOL, MCP_TOOLS_QUERY, MCP_TOOL_ADVANCED, PROVIDER_ID, SETTINGS_NAMESPACE, USER_AGENT, apply, inject, installSettingsSection, name, resolveApiKey, resolveApiKeyFromProcess, resolveOptions };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tonydua/dsh-web-search-exa",
3
3
  "description": "Zero-config Exa web search provider for DeepSeek Harness (dsh): keyless anonymous MCP fallback (mcp.exa.ai/mcp) plus keyed REST search — a drop-in WebSearchProvider for the ctx.web seam, no API key required.",
4
- "version": "0.1.5",
4
+ "version": "0.2.2",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -42,6 +42,13 @@
42
42
  "dsh": {
43
43
  "bundle": {
44
44
  "patch": "./cordis.patch.yml"
45
+ },
46
+ "compatibility": {
47
+ "dshReleases": {
48
+ "0.2.0-rc.1": "compatible",
49
+ "0.2.0-rc.2": "compatible",
50
+ "0.2.1-alpha.1": "compatible"
51
+ }
45
52
  }
46
53
  },
47
54
  "publishConfig": {
@@ -57,10 +64,10 @@
57
64
  "compat": "bash scripts/compat-matrix.sh"
58
65
  },
59
66
  "peerDependencies": {
60
- "@deepseek-ai/cordis": ">=4.0.2",
61
- "@deepseek-ai/dsh-launch-environment": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8",
62
- "@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8",
63
- "@deepseek-ai/dsh-web": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8"
67
+ "@deepseek-ai/cordis": ">=4.0.2 || >=4.0.5-alpha.1",
68
+ "@deepseek-ai/dsh-launch-environment": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8 || >=0.2.0-rc.1 || >=0.2.1-alpha.1",
69
+ "@deepseek-ai/dsh-settings": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8 || >=0.2.0-rc.1 || >=0.2.1-alpha.1",
70
+ "@deepseek-ai/dsh-web": ">=0.1.2-alpha.2 || >=0.1.3-alpha.2 || >=0.1.4-0 || >=0.1.5-alpha.1 || >=0.1.6-alpha.1 || >=0.1.7-alpha.1 || >=0.1.8 || >=0.2.0-rc.1 || >=0.2.1-alpha.1"
64
71
  },
65
72
  "peerDependenciesMeta": {
66
73
  "@deepseek-ai/dsh-settings": {