dsh-web-search-plugin 0.1.1 → 0.2.0

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/tavily.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Tavily REST search backend (`POST {baseURL}/search`).
3
+ * @module dsh-web-search-plugin/tavily
4
+ */
5
+ import { WebError } from "@deepseek-ai/dsh-web";
6
+ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
7
+ import { MAX_RESULTS_CAP, USER_AGENT, isAbortError, resolveApiKey, resolveSecret, searchAborted, throwIfSearchAborted } from "./shared.js";
8
+
9
+ /** Default Tavily REST API base; `/search` is appended. */
10
+ export const TAVILY_DEFAULT_BASE_URL = "https://api.tavily.com";
11
+ /** Default credential reference resolved for every keyed search. */
12
+ export const TAVILY_DEFAULT_API_KEY_ENV = "TAVILY_API_KEY";
13
+ /** Environment variable naming this backend's endpoint override. */
14
+ const TAVILY_BASE_URL_ENV = "TAVILY_BASE_URL";
15
+
16
+ /**
17
+ * Normalize a standard Tavily search response into the seam's
18
+ * `WebSearchResult`. Sources are deduped by `url`; the provider-generated
19
+ * `answer` becomes `content` only when `includeAnswer` asked for it.
20
+ * @param json - the parsed Tavily response body.
21
+ * @param includeAnswer - whether the request requested `include_answer`.
22
+ * @returns the normalized search result.
23
+ */
24
+ function mapTavilyResponse(json, includeAnswer) {
25
+ if (json === null || typeof json !== "object") throw new WebError("Tavily returned a non-object response body", "WEB_PROVIDER_ERROR");
26
+ const results = Array.isArray(json.results) ? json.results : [];
27
+ if (results.length === 0) throw new WebError("Tavily returned no results", "WEB_PROVIDER_ERROR");
28
+ const seen = /* @__PURE__ */ new Set();
29
+ const sources = [];
30
+ for (const item of results) {
31
+ if (item === null || typeof item !== "object") continue;
32
+ if (typeof item.url !== "string" || item.url.length === 0 || seen.has(item.url)) continue;
33
+ seen.add(item.url);
34
+ sources.push({
35
+ url: item.url,
36
+ ...typeof item.title === "string" && item.title.length > 0 ? { title: item.title } : {},
37
+ ...typeof item.content === "string" && item.content.length > 0 ? { snippet: item.content } : {},
38
+ ...typeof item.published_date === "string" && item.published_date.length > 0 ? { publishedAt: item.published_date } : {}
39
+ });
40
+ }
41
+ return {
42
+ ...includeAnswer === true && typeof json.answer === "string" && json.answer.length > 0 ? { content: json.answer } : {},
43
+ sources,
44
+ truncated: false
45
+ };
46
+ }
47
+
48
+ /** The Tavily-backed search backend; HTTP redirects fail as `WEB_PROVIDER_ERROR`. */
49
+ export class TavilySearchProvider {
50
+ constructor(resolveOptions) {
51
+ this.resolveOptions = resolveOptions;
52
+ }
53
+
54
+ available() {
55
+ const options = this.resolveOptions();
56
+ if (!URL.canParse(options.baseURL)) return false;
57
+ if (options.mode !== "keyed") return true;
58
+ return ((options.apiKey?.length ?? 0) > 0 || options.resolveApiKey !== void 0);
59
+ }
60
+
61
+ async search(request, signal) {
62
+ const options = this.resolveOptions();
63
+ throwIfSearchAborted(signal);
64
+ let apiKey;
65
+ if (options.mode === "keyed") {
66
+ apiKey = await resolveApiKey(options, signal, `Tavily search has no API key for "${options.apiKeyEnv ?? TAVILY_DEFAULT_API_KEY_ENV}"; store it through the credentials service, export it in the launching environment, or set a literal "apiKey" in the dsh-web-search-plugin config`);
67
+ throwIfSearchAborted(signal);
68
+ }
69
+ const endpoint = `${options.baseURL.replace(/\/+$/u, "")}/search`;
70
+ const body = {
71
+ query: request.query,
72
+ max_results: Math.min(request.maxResults ?? options.maxResults, MAX_RESULTS_CAP),
73
+ search_depth: options.searchDepth,
74
+ topic: options.topic,
75
+ include_answer: options.includeAnswer === true,
76
+ include_images: false
77
+ };
78
+ const headers = {
79
+ "content-type": "application/json",
80
+ "accept": "application/json",
81
+ "user-agent": USER_AGENT,
82
+ ...options.mode === "keyless" ? { "x-tavily-access-mode": "keyless" } : { "authorization": `Bearer ${apiKey}` }
83
+ };
84
+ let response;
85
+ try {
86
+ response = await fetch(endpoint, {
87
+ method: "POST",
88
+ redirect: "error",
89
+ headers,
90
+ body: JSON.stringify(body),
91
+ ...signal !== void 0 ? { signal } : {}
92
+ });
93
+ } catch (error) {
94
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
95
+ throw new WebError(`Tavily search request failed: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
96
+ }
97
+ if (!response.ok) {
98
+ let message = `Tavily API error (HTTP ${response.status})`;
99
+ try {
100
+ const parsed = await response.json();
101
+ const detail = parsed?.detail?.error ?? parsed?.error ?? parsed?.message;
102
+ if (typeof detail === "string" && detail.length > 0) message = detail;
103
+ } catch {
104
+ /* non-JSON error body — keep the status-line message */
105
+ }
106
+ throw new WebError(message, "WEB_PROVIDER_ERROR");
107
+ }
108
+ try {
109
+ return mapTavilyResponse(await response.json(), options.includeAnswer === true);
110
+ } catch (error) {
111
+ if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
112
+ if (error instanceof WebError) throw error;
113
+ throw new WebError(`Tavily returned an unprocessable response body: ${String(error)}`, "WEB_PROVIDER_ERROR", { cause: error });
114
+ }
115
+ }
116
+ }
117
+
118
+ /** Project the plugin section into Tavily backend options. */
119
+ export function resolveTavilyOptions(ctx, config) {
120
+ return {
121
+ mode: config.mode ?? "keyless",
122
+ ...resolveSecret(ctx, {
123
+ literal: config.apiKey,
124
+ envName: config.apiKeyEnv ?? TAVILY_DEFAULT_API_KEY_ENV
125
+ }),
126
+ baseURL: config.baseURL ?? launchEnvironmentOf(ctx).get(TAVILY_BASE_URL_ENV)?.value ?? TAVILY_DEFAULT_BASE_URL,
127
+ maxResults: config.maxResults ?? 8,
128
+ searchDepth: config.searchDepth ?? "basic",
129
+ includeAnswer: config.includeAnswer ?? true,
130
+ topic: config.topic ?? "general"
131
+ };
132
+ }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "dsh-web-search-plugin",
3
- "version": "0.1.1",
4
- "description": "Tavily web-search provider for the DeepSeek Harness web capability seam (ctx.web): keyless or API-key modes with a UI config card; provider framework extensible to more search APIs",
3
+ "version": "0.2.0",
4
+ "description": "Unified web search for the DeepSeek Harness seam (ctx.web): DeepSeek (official), Tavily, and Brave backends with an in-browser selector",
5
5
  "keywords": [
6
6
  "dsh-plugin",
7
7
  "deepseek-harness",
8
8
  "dsh",
9
9
  "tavily",
10
+ "brave",
11
+ "brave-search",
10
12
  "web-search",
11
13
  "websearch",
12
14
  "search",
@@ -16,7 +18,10 @@
16
18
  "main": "lib/index.js",
17
19
  "files": [
18
20
  "lib",
19
- "CHANGELOG.md"
21
+ "cordis.patch.yml",
22
+ "CHANGELOG.md",
23
+ "README.md",
24
+ "LICENSE"
20
25
  ],
21
26
  "engines": {
22
27
  "node": ">=18"
@@ -28,9 +33,13 @@
28
33
  "./client": {
29
34
  "default": "./lib/client.js"
30
35
  },
36
+ "./cordis.patch.yml": "./cordis.patch.yml",
31
37
  "./package.json": "./package.json"
32
38
  },
33
39
  "dsh": {
40
+ "bundle": {
41
+ "patch": "./cordis.patch.yml"
42
+ },
34
43
  "client": {
35
44
  "platform": "web",
36
45
  "inject": [
@@ -44,7 +53,10 @@
44
53
  }
45
54
  },
46
55
  "scripts": {
47
- "check": "node --check lib/index.js && node --check lib/client.js"
56
+ "check": "node --check lib/index.js && node --check lib/client.js && node --check lib/shared.js && node --check lib/tavily.js && node --check lib/brave.js && node --check lib/deepseek.js"
57
+ },
58
+ "dependencies": {
59
+ "undici": "^6.21.0"
48
60
  },
49
61
  "peerDependencies": {
50
62
  "@deepseek-ai/dsh-web": "^0.1.0-rc.7",