@tonydua/dsh-web-search-exa 0.1.2 → 0.1.5

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 ADDED
@@ -0,0 +1,386 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { WebError, WebSearchProvider, WebSearchRequest, WebSearchResult } from "@deepseek-ai/dsh-web";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+ //#region src/types.d.ts
5
+ /**
6
+ * Wire and configuration types for `@tonydua/dsh-web-search-exa`.
7
+ *
8
+ * Two independent wire shapes live here: Exa's REST `POST /search` response
9
+ * (used when an API key is configured) and the normalized MCP payload returned
10
+ * by Exa's hosted MCP server (the anonymous, keyless path). Neither is exported
11
+ * to consumers — the provider normalizes both into the seam's
12
+ * `WebSearchResult` vocabulary.
13
+ *
14
+ * @module @tonydua/dsh-web-search-exa/types
15
+ */
16
+ /** Retrieval mode sent to Exa's REST `type` field. */
17
+ type ExaSearchType = 'auto' | 'keyword' | 'neural';
18
+ /** One `results[]` entry of Exa's REST response (fields we actually read). */
19
+ interface ExaRestResult {
20
+ readonly url: string;
21
+ readonly title?: string | null;
22
+ /** Exa's per-result highlight sentences; the only portable snippet source. */
23
+ readonly highlights?: readonly string[] | null;
24
+ /** Exa's publication/crawl timestamp, passed through as `publishedAt`. */
25
+ readonly publishedDate?: string | null;
26
+ }
27
+ /** Exa's REST `POST /search` response body. */
28
+ interface ExaRestResponse {
29
+ readonly results?: readonly ExaRestResult[] | null;
30
+ }
31
+ /**
32
+ * One `content[]` block of a normalized MCP result payload.
33
+ * Non-text blocks (images, resources) are skipped by the collector.
34
+ */
35
+ interface McpContentItem {
36
+ readonly type?: string;
37
+ readonly text?: string;
38
+ }
39
+ /** The `result` member of a JSON-RPC 2.0 response to a `tools/call`. */
40
+ interface McpToolResult {
41
+ readonly content?: readonly McpContentItem[] | null;
42
+ /** True when the tool itself reported a failure (as opposed to a transport error). */
43
+ readonly isError?: boolean;
44
+ }
45
+ /** The `error` member of a JSON-RPC 2.0 response. */
46
+ interface McpJsonRpcError {
47
+ readonly code?: number;
48
+ readonly message?: string;
49
+ }
50
+ /** A JSON-RPC 2.0 envelope as returned by Exa's hosted MCP server. */
51
+ interface McpPayload {
52
+ readonly result?: McpToolResult | null;
53
+ readonly error?: McpJsonRpcError | null;
54
+ }
55
+ /**
56
+ * One parsed `Title:`-led section of Exa MCP text output. Every field is
57
+ * best-effort: Exa omits or emits `N/A` for several of them.
58
+ */
59
+ interface ExaMcpSection {
60
+ url?: string;
61
+ title?: string;
62
+ publishedAt?: string;
63
+ author?: string;
64
+ highlights?: string[];
65
+ /** Full-page text, used only as a snippet fallback when no highlight exists. */
66
+ text?: string;
67
+ }
68
+ //#endregion
69
+ //#region src/provider.d.ts
70
+ /**
71
+ * Fully resolved options the provider serves one search with. Produced by
72
+ * {@link resolveOptions} from the current Settings section, so every field is
73
+ * already defaulted by the time the provider reads it.
74
+ *
75
+ * The optional members are declared `| undefined` rather than merely optional:
76
+ * `resolveOptions` always sets them (possibly to `undefined`), and this
77
+ * package type-checks under `exactOptionalPropertyTypes`.
78
+ */
79
+ interface ExaSearchProviderOptions {
80
+ readonly providerId?: string | undefined;
81
+ readonly apiKey: string;
82
+ readonly apiKeyEnv: string;
83
+ readonly baseURL: string;
84
+ readonly apiURL?: string | undefined;
85
+ readonly mcpURL: string;
86
+ readonly searchType: ExaSearchType;
87
+ readonly numResults?: number | undefined;
88
+ readonly highlightsPerResult: number;
89
+ }
90
+ /** The dsh Settings section shape (fields optional at the load boundary). */
91
+ interface ExaSearchProviderConfig {
92
+ providerId?: string;
93
+ apiKey?: string;
94
+ apiKeyEnv?: string;
95
+ baseURL?: string;
96
+ /** @deprecated Use `baseURL`; this full endpoint remains supported for compatibility. */
97
+ apiURL?: string;
98
+ mcpURL?: string;
99
+ searchType?: ExaSearchType;
100
+ numResults?: number;
101
+ highlightsPerResult?: number;
102
+ }
103
+ /**
104
+ * The launch-environment snapshot the provider resolves credentials against.
105
+ * Structural rather than the concrete dsh type so direct library use can pass
106
+ * any `get`-shaped source.
107
+ */
108
+ interface ExaKeyEnvironment {
109
+ get(name: string): {
110
+ value: string;
111
+ } | undefined;
112
+ }
113
+ /** Resolve one search's API key; `undefined` selects the anonymous MCP path. */
114
+ type ExaApiKeyResolver = (options: ExaSearchProviderOptions) => string | undefined;
115
+ /** Options thunk: called per operation so live Settings edits take effect next search. */
116
+ type ExaOptionsResolver = () => ExaSearchProviderOptions;
117
+ /**
118
+ * Resolve the API key: literal config first, then the environment variable.
119
+ * `undefined` means the anonymous MCP path is used.
120
+ */
121
+ export declare function resolveApiKeyFromProcess(options: ExaSearchProviderOptions): string | undefined;
122
+ /**
123
+ * Resolve a key against dsh's immutable launch-environment snapshot. The
124
+ * process fallback keeps direct library use and older hosts working.
125
+ */
126
+ export declare function resolveApiKey(options: ExaSearchProviderOptions, environment?: ExaKeyEnvironment): string | undefined;
127
+ /**
128
+ * How long a tripped breaker keeps `available()` false before the next search
129
+ * is allowed to probe the anonymous endpoint again.
130
+ */
131
+ export declare const DEFAULT_BREAKER_COOLDOWN_MS = 300000;
132
+ /**
133
+ * Consecutive transient failures that trip the breaker.
134
+ *
135
+ * Only 5xx, 429, and network-level failures count: they say the anonymous
136
+ * endpoint is having a bad time, not that the request was wrong. A 4xx (other
137
+ * than 429) is a configuration error and would fail identically forever, so it
138
+ * deliberately does NOT trip the breaker — hiding a bad endpoint behind a
139
+ * cooldown would just delay the same error.
140
+ */
141
+ export declare const DEFAULT_BREAKER_THRESHOLD = 3;
142
+ /** A failure worth retrying later, as opposed to a permanent configuration error. */
143
+ export declare class ExaTransientError extends WebError {
144
+ constructor(message: string, cause?: unknown);
145
+ }
146
+ /**
147
+ * A rate limit, kept distinct from a generic provider failure so the model (and
148
+ * a human reading the transcript) can tell "Exa is throttling the keyless
149
+ * channel, configure a key" apart from "the network is broken".
150
+ *
151
+ * `code` is an open string in the seam's vocabulary, so a plugin-specific code
152
+ * is the supported way to route this; consumers must tolerate unknown codes.
153
+ */
154
+ export declare class ExaRateLimitError extends WebError {
155
+ constructor(message: string);
156
+ }
157
+ /**
158
+ * Consecutive-transient-failure breaker for the keyless path.
159
+ *
160
+ * The public MCP endpoint is best-effort: when it is throttling or down, every
161
+ * search would otherwise fail. Reporting that state through {@link
162
+ * ExaSearchProvider.available} is what lets a deployment recover — the seam
163
+ * skips an unavailable provider, so an unconfigured profile falls back to
164
+ * another registered provider instead of surfacing a hard error.
165
+ *
166
+ * State is per provider instance (one per plugin mount) and never persisted:
167
+ * a restart is a fresh chance, and one successful search resets the count.
168
+ */
169
+ export declare class ExaAvailabilityBreaker {
170
+ #private;
171
+ constructor(threshold?: number, cooldownMs?: number);
172
+ /** True while the breaker is open and the cooldown has not elapsed. */
173
+ get blocked(): boolean;
174
+ /** Record one successful operation: the endpoint is healthy again. */
175
+ succeeded(): void;
176
+ /** Record one transient failure, opening the breaker at the threshold. */
177
+ failed(): void;
178
+ }
179
+ /**
180
+ * Project one resolved configuration section into the options the provider
181
+ * serves its next search with. Called per operation so live Settings edits
182
+ * take effect on the next search.
183
+ */
184
+ export declare function resolveOptions(section: ExaSearchProviderConfig): ExaSearchProviderOptions;
185
+ /**
186
+ * Exa-backed search with an anonymous fallback.
187
+ *
188
+ * Path selection is per search, not per install: a key appearing later (for
189
+ * example after a Settings edit) upgrades the next search to REST without a
190
+ * restart, and removing it falls back to the anonymous endpoint.
191
+ */
192
+ export declare class ExaSearchProvider implements WebSearchProvider {
193
+ #private;
194
+ /**
195
+ * @param resolveOptions - thunk returning the options for the NEXT
196
+ * operation, snapshotted once at each operation's entry so one search
197
+ * never mixes two settings sections (same pattern as the official
198
+ * DeepSeek provider).
199
+ * @param resolveApiKey - optional key resolver; dsh hosts pass their
200
+ * launch-environment snapshot while direct users retain process.env fallback.
201
+ * @param breaker - health tracker for the keyless path; injectable so tests
202
+ * need no clock control.
203
+ */
204
+ constructor(resolveOptions: ExaOptionsResolver, resolveApiKey?: ExaApiKeyResolver, breaker?: ExaAvailabilityBreaker);
205
+ /**
206
+ * Read per operation rather than frozen at construction: a Settings edit to
207
+ * `providerId` must not leave the provider reporting an id the registry does
208
+ * not key it under. Registering under the new id is the user's job (the
209
+ * loader re-reads config on reload), but the reported value stays honest.
210
+ */
211
+ get id(): string;
212
+ /**
213
+ * Cheap local usability check — no network call, per the seam contract.
214
+ *
215
+ * False when the local options are unusable, or while the keyless channel's
216
+ * breaker is open after repeated transient failures. Reporting that honestly
217
+ * is what lets the seam fall back to another provider instead of failing the
218
+ * search: a pinned `searchProvider` surfaces
219
+ * `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`, an unpinned one simply selects
220
+ * another registered provider.
221
+ */
222
+ available(): boolean;
223
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
224
+ }
225
+ //#endregion
226
+ //#region src/constants.d.ts
227
+ /**
228
+ * Defaults and stable identifiers for `@tonydua/dsh-web-search-exa`.
229
+ *
230
+ * Every value here is also restated in the Settings schema with the same
231
+ * default, so a reader can see the effective value without following the
232
+ * import. Keep the two in sync: the schema is the user-facing contract, these
233
+ * are the values the provider falls back to when a field is absent.
234
+ *
235
+ * @module @tonydua/dsh-web-search-exa/constants
236
+ */
237
+ /** Default provider id this provider registers under (`ctx.web` registry key). */
238
+ export declare const DEFAULT_PROVIDER_ID = "exa";
239
+ /** Backward-compatible alias for the default provider id. */
240
+ export declare const PROVIDER_ID = "exa";
241
+ /** Exa REST search endpoint; used only when an API key is configured. */
242
+ export declare const DEFAULT_BASE_URL = "https://api.exa.ai";
243
+ /** Legacy full REST endpoint; `baseURL` is the canonical dsh-compatible option. */
244
+ 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";
247
+ /** Environment variable consulted when no literal `apiKey` is configured. */
248
+ export declare const DEFAULT_API_KEY_ENV = "EXA_API_KEY";
249
+ /** Default retrieval mode for the REST path: let Exa pick. */
250
+ export declare const DEFAULT_SEARCH_TYPE = "auto";
251
+ /** Default number of highlight sentences requested per result (REST path). */
252
+ export declare const DEFAULT_HIGHLIGHTS_PER_RESULT = 1;
253
+ /** MCP tool name for plain web search on Exa's hosted server. */
254
+ export declare const MCP_TOOL = "web_search_exa";
255
+ /**
256
+ * Attribution header sent on anonymous MCP requests. This is the only signal
257
+ * Exa's public endpoint receives about the caller, so it is deliberately a
258
+ * product-level name rather than a per-install identifier.
259
+ */
260
+ export declare const MCP_SOURCE = "dsh-anything";
261
+ /**
262
+ * User agent for REST requests.
263
+ *
264
+ * Annotated `: string` rather than left inferred: this constant is re-exported
265
+ * from the package root, and a `const` string infers a LITERAL type, which
266
+ * would bake today's version number into every consumer's type-checking.
267
+ */
268
+ export declare const USER_AGENT: string;
269
+ /** Snippet cap for text-derived snippets (matching oh-my-pi's choice). */
270
+ export declare const MAX_SNIPPET_CHARS = 500;
271
+ /** Settings namespace carrying this provider's configuration. */
272
+ export declare const SETTINGS_NAMESPACE = "web-search-exa";
273
+ //#endregion
274
+ //#region src/index.d.ts
275
+ declare const Config: z<Schemastery.ObjectS<{
276
+ /**
277
+ * Provider id registered into `ctx.web`. Defaults to `exa` (same as the
278
+ * official `@deepseek-ai/dsh-web-search-exa`). Change it only when BOTH
279
+ * packages are installed in one profile — the seam rejects duplicate ids
280
+ * with `WEB_DUPLICATE_PROVIDER`. There is no silent override: pick a
281
+ * distinct id here (e.g. `exa-anon`) and select it explicitly with
282
+ * `searchProvider` / `$DSH_WEB_SEARCH_PROVIDER`.
283
+ */
284
+ providerId: z<string, string>;
285
+ /** Literal Exa API key; an empty/missing value enables the anonymous MCP path. */
286
+ apiKey: z<string, string>;
287
+ /** Environment variable consulted when no literal `apiKey` is configured. */
288
+ apiKeyEnv: z<string, string>;
289
+ /** Exa API base URL; `/search` is appended for the keyed REST path. */
290
+ baseURL: z<string, string>;
291
+ /**
292
+ * Legacy full REST endpoint. When set, it takes precedence over `baseURL`;
293
+ * new configurations should use `baseURL` to match the official provider.
294
+ */
295
+ apiURL: z<string, string>;
296
+ /** Exa hosted MCP endpoint, used by the anonymous fallback. */
297
+ mcpURL: z<string, string>;
298
+ /** REST retrieval mode: `auto`, `keyword`, or `neural`. */
299
+ searchType: z<"auto" | "keyword" | "neural", "auto" | "keyword" | "neural">;
300
+ /** Default result count when the request carries no `maxResults`. */
301
+ numResults: z<number, number>;
302
+ /** Highlight sentences requested per result on the REST path. */
303
+ highlightsPerResult: z<number, number>;
304
+ }>, Schemastery.ObjectT<{
305
+ /**
306
+ * Provider id registered into `ctx.web`. Defaults to `exa` (same as the
307
+ * official `@deepseek-ai/dsh-web-search-exa`). Change it only when BOTH
308
+ * packages are installed in one profile — the seam rejects duplicate ids
309
+ * with `WEB_DUPLICATE_PROVIDER`. There is no silent override: pick a
310
+ * distinct id here (e.g. `exa-anon`) and select it explicitly with
311
+ * `searchProvider` / `$DSH_WEB_SEARCH_PROVIDER`.
312
+ */
313
+ providerId: z<string, string>;
314
+ /** Literal Exa API key; an empty/missing value enables the anonymous MCP path. */
315
+ apiKey: z<string, string>;
316
+ /** Environment variable consulted when no literal `apiKey` is configured. */
317
+ apiKeyEnv: z<string, string>;
318
+ /** Exa API base URL; `/search` is appended for the keyed REST path. */
319
+ baseURL: z<string, string>;
320
+ /**
321
+ * Legacy full REST endpoint. When set, it takes precedence over `baseURL`;
322
+ * new configurations should use `baseURL` to match the official provider.
323
+ */
324
+ apiURL: z<string, string>;
325
+ /** Exa hosted MCP endpoint, used by the anonymous fallback. */
326
+ mcpURL: z<string, string>;
327
+ /** REST retrieval mode: `auto`, `keyword`, or `neural`. */
328
+ searchType: z<"auto" | "keyword" | "neural", "auto" | "keyword" | "neural">;
329
+ /** Default result count when the request carries no `maxResults`. */
330
+ numResults: z<number, number>;
331
+ /** Highlight sentences requested per result on the REST path. */
332
+ highlightsPerResult: z<number, number>;
333
+ }>>;
334
+ /** Cordis plugin name used by loader diagnostics. */
335
+ export declare const name = "web-search-exa";
336
+ /** The web seam this provider registers into. */
337
+ export declare const inject: readonly ["web"];
338
+ /**
339
+ * The subset of the settings service this plugin uses, described structurally
340
+ * rather than by the concrete `dsh-settings` class, because that API changed
341
+ * shape and the plugin must work on both sides of the change.
342
+ *
343
+ * - **dsh ≤ 0.1.6** exposes `SettingsProvider.installSection`, which registered
344
+ * a namespace and — the part the provider actually depends on — handed back
345
+ * the authoritative section thunk through `setSource`.
346
+ * - **dsh ≥ 0.1.7** replaces that with `SettingsForms`, which derives a page
347
+ * from the Config schema the Loader already holds for this entry
348
+ * (`SettingsDescriptor.schema`, `autoGenerate`). There is no namespace to
349
+ * install, so this plugin has nothing to register and must simply not
350
+ * crash.
351
+ *
352
+ * Both members are optional: a host with neither still gets a working provider,
353
+ * only without live Settings-driven reconfiguration.
354
+ */
355
+ interface SettingsServiceLike {
356
+ installSection?: (owner: unknown, ns: string, schema: unknown, entry: unknown, hooks: {
357
+ setSource: (source: () => ExaSearchProviderConfig) => void;
358
+ onChange: () => void;
359
+ }) => void;
360
+ }
361
+ /**
362
+ * Wire the settings service when the host exposes the pre-0.1.7 registration
363
+ * API; do nothing (rather than throw) on hosts that do not.
364
+ *
365
+ * @param settings - the mounted settings service, of either generation.
366
+ * @param owner - the consuming context `installSection` attributes the section to.
367
+ * @param config - the composition entry, used as the section's base value.
368
+ * @param adopt - receives the authoritative section thunk so later searches read
369
+ * live edits instead of the boot-time config.
370
+ * @returns true when a section was installed.
371
+ */
372
+ declare function installSettingsSection(settings: SettingsServiceLike, owner: unknown, config: ExaSearchProviderConfig, adopt: (source: () => ExaSearchProviderConfig) => void): boolean;
373
+ /**
374
+ * Register the Exa search provider with `ctx.web` and, when the settings
375
+ * service is mounted *and* exposes the pre-0.1.7 registration API, install its
376
+ * Settings section.
377
+ *
378
+ * The settings work is deliberately inside `ctx.inject`, so a profile that
379
+ * omits `dsh-settings` still mounts the provider — keyless search must not
380
+ * depend on the Settings UI being present. The provider registration happens
381
+ * outside it for the same reason, and is never conditional on the settings
382
+ * generation.
383
+ */
384
+ export declare function apply(ctx: Context, config: ExaSearchProviderConfig): void;
385
+ //#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 };