webfox 4.8.0 → 4.9.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  trimSnippet
3
- } from "./chunk-X6ZBIHZC.js";
3
+ } from "./chunk-FSTCLBSF.js";
4
4
  import {
5
5
  httpError
6
6
  } from "./chunk-U4BLULLV.js";
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  formatJson,
11
11
  trimSnippet
12
- } from "./chunk-X6ZBIHZC.js";
12
+ } from "./chunk-FSTCLBSF.js";
13
13
  import {
14
14
  orderedContents
15
15
  } from "./chunk-YZXM4U2C.js";
@@ -78,14 +78,18 @@ var valyuImplementation = {
78
78
  error: "No content returned for this URL."
79
79
  };
80
80
  }
81
- return result.status === "failed" ? {
82
- url,
83
- error: result.error ?? formatJson(result)
84
- } : {
85
- url: result.url,
86
- ...typeof result.content === "string" || typeof result.content === "number" ? { content: String(result.content) } : {},
87
- ...result.summary !== void 0 ? { summary: result.summary } : {},
88
- metadata: result
81
+ if (result.status === "failed" || result.status === "not_found") {
82
+ return {
83
+ url,
84
+ error: result.error ?? formatJson(result)
85
+ };
86
+ }
87
+ const page = result;
88
+ return {
89
+ url: page.url,
90
+ ...typeof page.content === "string" || typeof page.content === "number" ? { content: String(page.content) } : {},
91
+ ...page.summary !== void 0 ? { summary: page.summary } : {},
92
+ metadata: page
89
93
  };
90
94
  })
91
95
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  trimSnippet
3
- } from "./chunk-X6ZBIHZC.js";
3
+ } from "./chunk-FSTCLBSF.js";
4
4
  import {
5
5
  httpError
6
6
  } from "./chunk-U4BLULLV.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  asJsonObject,
3
3
  trimSnippet
4
- } from "./chunk-X6ZBIHZC.js";
4
+ } from "./chunk-FSTCLBSF.js";
5
5
  import {
6
6
  httpError
7
7
  } from "./chunk-U4BLULLV.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  formatJson,
3
3
  trimSnippet
4
- } from "./chunk-X6ZBIHZC.js";
4
+ } from "./chunk-FSTCLBSF.js";
5
5
  import {
6
6
  orderedContents
7
7
  } from "./chunk-YZXM4U2C.js";
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  formatJson,
3
+ joinUrl,
4
+ requestJson,
3
5
  trimSnippet
4
- } from "./chunk-X6ZBIHZC.js";
6
+ } from "./chunk-FSTCLBSF.js";
5
7
  import {
6
8
  orderedContents
7
9
  } from "./chunk-YZXM4U2C.js";
@@ -10,15 +12,19 @@ import {
10
12
  } from "./chunk-U4BLULLV.js";
11
13
 
12
14
  // src/providers/exa/adapter.ts
13
- import { Exa as ExaClient } from "exa-js";
15
+ var DEFAULT_BASE_URL = "https://api.exa.ai";
16
+ var DEFAULT_MAX_CHARACTERS = 1e4;
14
17
  var exaImplementation = {
15
- async search(query, maxResults, config, _context, searchOptions) {
16
- const client = createClient(config);
17
- const options = {
18
- ...searchOptions ?? {},
19
- numResults: maxResults
20
- };
21
- const response = await client.search(query, options);
18
+ async search(query, maxResults, config, context, searchOptions) {
19
+ const response = await exaRequest(
20
+ config,
21
+ "/search",
22
+ buildSearchBody(query, {
23
+ ...searchOptions ?? {},
24
+ numResults: maxResults
25
+ }),
26
+ context
27
+ );
22
28
  return {
23
29
  provider: "exa",
24
30
  results: (response.results ?? []).slice(0, maxResults).map((result) => ({
@@ -31,9 +37,13 @@ var exaImplementation = {
31
37
  }))
32
38
  };
33
39
  },
34
- async contents(urls, config, _context, options) {
35
- const client = createClient(config);
36
- const response = await client.getContents(urls, options);
40
+ async contents(urls, config, context, options) {
41
+ const response = await exaRequest(
42
+ config,
43
+ "/contents",
44
+ { urls, ...options ?? {} },
45
+ context
46
+ );
37
47
  const results = response.results ?? [];
38
48
  return {
39
49
  provider: "exa",
@@ -54,9 +64,21 @@ var exaImplementation = {
54
64
  })
55
65
  };
56
66
  },
57
- async answer(query, config, _context, options) {
58
- const client = createClient(config);
59
- const response = await client.answer(query, options);
67
+ async answer(query, config, context, options) {
68
+ const response = await exaRequest(
69
+ config,
70
+ "/answer",
71
+ {
72
+ query,
73
+ stream: false,
74
+ text: options?.text ?? false,
75
+ model: options?.model ?? "exa",
76
+ systemPrompt: options?.systemPrompt,
77
+ outputSchema: options?.outputSchema,
78
+ userLocation: options?.userLocation
79
+ },
80
+ context
81
+ );
60
82
  const lines = [];
61
83
  lines.push(
62
84
  typeof response.answer === "string" ? response.answer : formatJson(response.answer)
@@ -81,15 +103,19 @@ var exaImplementation = {
81
103
  async research(input, config, context, options) {
82
104
  context.signal?.throwIfAborted();
83
105
  context.onProgress?.("Exa is researching and synthesizing the report.");
84
- const client = createClient(config);
85
- const result = await client.search(input, {
86
- ...options ?? {},
87
- type: "deep-reasoning",
88
- outputSchema: {
89
- type: "text",
90
- description: "A detailed Markdown research report answering the query. Cite sources with links, distinguish evidence from uncertainty, and include a conclusion."
91
- }
92
- });
106
+ const result = await exaRequest(
107
+ config,
108
+ "/search",
109
+ buildSearchBody(input, {
110
+ ...options ?? {},
111
+ type: "deep-reasoning",
112
+ outputSchema: {
113
+ type: "text",
114
+ description: "A detailed Markdown research report answering the query. Cite sources with links, distinguish evidence from uncertainty, and include a conclusion."
115
+ }
116
+ }),
117
+ context
118
+ );
93
119
  const content = result.output?.content;
94
120
  if (typeof content !== "string" || !content.trim())
95
121
  throw new WebfoxError(
@@ -113,12 +139,42 @@ var exaImplementation = {
113
139
  };
114
140
  }
115
141
  };
116
- function createClient(config) {
142
+ function buildSearchBody(query, options) {
143
+ const { stream: _stream, ...rest } = options;
144
+ if (!("contents" in rest)) {
145
+ return {
146
+ query,
147
+ ...rest,
148
+ contents: { text: { maxCharacters: DEFAULT_MAX_CHARACTERS } }
149
+ };
150
+ }
151
+ if (rest.contents === false || rest.contents == null) {
152
+ const { contents: _contents, ...withoutContents } = rest;
153
+ return { query, ...withoutContents };
154
+ }
155
+ return { query, ...rest };
156
+ }
157
+ async function exaRequest(config, path, body, context) {
117
158
  const apiKey = config.credentials?.api;
118
159
  if (!apiKey) {
119
160
  throw new Error("is missing an API key");
120
161
  }
121
- return new ExaClient(apiKey, config.baseUrl);
162
+ return await requestJson(
163
+ joinUrl(config.baseUrl ?? DEFAULT_BASE_URL, path),
164
+ { headers: { "x-api-key": apiKey }, body, signal: context.signal },
165
+ readExaError
166
+ );
167
+ }
168
+ function readExaError(payload, response) {
169
+ if (typeof payload !== "object" || payload === null) {
170
+ const text = typeof payload === "string" ? payload.trim().slice(0, 500) : "";
171
+ return text ? `Request failed with status ${response.status}: ${text}` : `Request failed with status ${response.status}.`;
172
+ }
173
+ const record = payload;
174
+ const envelope = typeof record.error === "object" && record.error !== null ? record.error : void 0;
175
+ const parts = envelope ? [envelope.message ?? record.message] : [record.error, record.message];
176
+ const message = parts.filter((part) => typeof part === "string" && part !== "").join(". ");
177
+ return message || `Request failed with status ${response.status}.`;
122
178
  }
123
179
  var adapter = {
124
180
  async search(input, config, context) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  normalizeContentText,
3
3
  trimSnippet
4
- } from "./chunk-X6ZBIHZC.js";
4
+ } from "./chunk-FSTCLBSF.js";
5
5
  import {
6
6
  orderedContents
7
7
  } from "./chunk-YZXM4U2C.js";
@@ -4,45 +4,45 @@ import {
4
4
  import "./chunk-I2UIACY3.js";
5
5
  import "./chunk-WCTOYZVB.js";
6
6
  import {
7
+ expectJsonBody,
7
8
  formatJson,
9
+ joinUrl,
10
+ readResponseBody,
8
11
  trimSnippet
9
- } from "./chunk-X6ZBIHZC.js";
12
+ } from "./chunk-FSTCLBSF.js";
10
13
  import {
11
14
  orderedContents
12
15
  } from "./chunk-YZXM4U2C.js";
13
16
  import {
17
+ WebfoxError,
14
18
  asWebfoxError
15
19
  } from "./chunk-U4BLULLV.js";
16
20
 
17
21
  // src/providers/linkup/adapter.ts
18
- import {
19
- LinkupClient
20
- } from "linkup-sdk";
22
+ var DEFAULT_BASE_URL = "https://api.linkup.so/v1";
21
23
  var linkupImplementation = {
22
- async search(query, maxResults, config, _context, options) {
23
- const client = createClient(config);
24
- const response = await client.search(
25
- buildSearchParams(query, maxResults, {
24
+ async search(query, maxResults, config, context, options) {
25
+ const response = await linkupRequest(config, context, "/search", {
26
+ body: buildSearchParams(query, maxResults, {
26
27
  ...options ?? {}
27
28
  })
28
- );
29
+ });
29
30
  return {
30
31
  provider: "linkup",
31
32
  results: (response.results ?? []).map(toSearchResult).filter((result) => result !== null).slice(0, maxResults)
32
33
  };
33
34
  },
34
- async contents(urls, config, _context, options) {
35
- const client = createClient(config);
35
+ async contents(urls, config, context, options) {
36
36
  return {
37
37
  provider: "linkup",
38
38
  answers: await Promise.all(
39
39
  urls.map(async (url) => {
40
40
  try {
41
- const response = await client.fetch(
42
- buildFetchParams(url, {
41
+ const response = await linkupRequest(config, context, "/fetch", {
42
+ body: buildFetchParams(url, {
43
43
  ...options ?? {}
44
44
  })
45
- );
45
+ });
46
46
  const metadata = Object.fromEntries(
47
47
  Object.entries(response).filter(
48
48
  ([key, value]) => [
@@ -86,18 +86,20 @@ var linkupImplementation = {
86
86
  poll: (id, researchContext) => linkupImplementation.pollResearch(id, config, researchContext)
87
87
  });
88
88
  },
89
- async startResearch(input, config, _context, options) {
90
- const client = createClient(config);
91
- const task = await client.research(
92
- buildResearchParams(input, {
89
+ async startResearch(input, config, context, options) {
90
+ const task = await linkupRequest(config, context, "/research", {
91
+ body: buildResearchParams(input, {
93
92
  ...options ?? {}
94
93
  })
95
- );
94
+ });
96
95
  return { id: task.id };
97
96
  },
98
- async pollResearch(id, config, _context) {
99
- const client = createClient(config);
100
- const task = await client.getResearch(id);
97
+ async pollResearch(id, config, context) {
98
+ const task = await linkupRequest(
99
+ config,
100
+ context,
101
+ `/research/${encodeURIComponent(id)}`
102
+ );
101
103
  if (task.status === "completed") {
102
104
  return {
103
105
  status: "completed",
@@ -135,7 +137,7 @@ function buildSearchParams(query, maxResults, options) {
135
137
  );
136
138
  }
137
139
  return {
138
- query,
140
+ q: query,
139
141
  depth: searchOptions.depth ?? "standard",
140
142
  outputType: "searchResults",
141
143
  maxResults,
@@ -179,7 +181,7 @@ function buildResearchParams(input, options) {
179
181
  );
180
182
  }
181
183
  const commonParams = {
182
- query: input,
184
+ q: input,
183
185
  ...researchOptions.includeDomains !== void 0 ? { includeDomains: researchOptions.includeDomains } : {},
184
186
  ...researchOptions.excludeDomains !== void 0 ? { excludeDomains: researchOptions.excludeDomains } : {},
185
187
  ...researchOptions.fromDate !== void 0 ? { fromDate: toDate(researchOptions.fromDate, "fromDate") } : {},
@@ -191,7 +193,9 @@ function buildResearchParams(input, options) {
191
193
  return {
192
194
  ...commonParams,
193
195
  outputType,
194
- structuredOutputSchema: researchOptions.structuredOutputSchema
196
+ structuredOutputSchema: serializeSchema(
197
+ researchOptions.structuredOutputSchema
198
+ )
195
199
  };
196
200
  }
197
201
  return {
@@ -199,14 +203,50 @@ function buildResearchParams(input, options) {
199
203
  outputType
200
204
  };
201
205
  }
202
- function createClient(config) {
206
+ function serializeSchema(schema) {
207
+ return typeof schema === "string" ? schema : JSON.stringify(schema);
208
+ }
209
+ async function linkupRequest(config, context, path, init = {}) {
203
210
  const apiKey = config.credentials?.api;
204
211
  if (!apiKey) {
205
212
  throw new Error("is missing an API key");
206
213
  }
207
- return new LinkupClient({
208
- apiKey,
209
- baseUrl: config.baseUrl
214
+ const response = await fetch(
215
+ joinUrl(config.baseUrl ?? DEFAULT_BASE_URL, path),
216
+ {
217
+ method: init.body === void 0 ? "GET" : "POST",
218
+ headers: {
219
+ Accept: "application/json",
220
+ Authorization: `Bearer ${apiKey}`,
221
+ ...init.body === void 0 ? {} : { "Content-Type": "application/json" }
222
+ },
223
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
224
+ signal: context.signal
225
+ }
226
+ );
227
+ const payload = await readResponseBody(response);
228
+ if (!response.ok) {
229
+ throw linkupError(response, payload);
230
+ }
231
+ return expectJsonBody(payload, response);
232
+ }
233
+ var NON_RETRYABLE_CODES = /* @__PURE__ */ new Set([
234
+ "INSUFFICIENT_FUNDS_CREDITS",
235
+ "EXCEED_BUDGET_LIMIT"
236
+ ]);
237
+ function linkupError(response, payload) {
238
+ const error = asRecord(asRecord(payload)?.error);
239
+ const code = readString(error?.code);
240
+ const details = Array.isArray(error?.details) ? error.details : [];
241
+ const message = [
242
+ readString(error?.message) ?? (typeof payload === "string" && payload.trim() ? payload.trim().slice(0, 500) : `Linkup request failed with status ${response.status}.`),
243
+ ...details.flatMap((detail) => {
244
+ const text = readString(asRecord(detail)?.message);
245
+ return text ? [text] : [];
246
+ })
247
+ ].join(" ");
248
+ return new WebfoxError("PROVIDER_FAILURE", message, {
249
+ retryable: [408, 429, 500, 502, 503, 504].includes(response.status) && !(code && NON_RETRYABLE_CODES.has(code))
210
250
  });
211
251
  }
212
252
  function formatResearchTaskOutput(task) {
@@ -298,7 +338,7 @@ function toDate(value, name) {
298
338
  `Linkup option '${name}' must be a valid date string, timestamp, or Date.`
299
339
  );
300
340
  }
301
- return date;
341
+ return date.toISOString().slice(0, 10);
302
342
  }
303
343
  var adapter = {
304
344
  async search(input, config, context) {
@@ -1,52 +1,65 @@
1
1
  import {
2
2
  formatJson,
3
+ joinUrl,
4
+ requestJson,
3
5
  trimSnippet
4
- } from "./chunk-X6ZBIHZC.js";
6
+ } from "./chunk-FSTCLBSF.js";
5
7
  import {
6
8
  orderedContents
7
9
  } from "./chunk-YZXM4U2C.js";
8
10
  import {
9
11
  WebfoxError,
10
- asWebfoxError,
11
- httpError
12
+ asWebfoxError
12
13
  } from "./chunk-U4BLULLV.js";
13
14
 
14
15
  // src/providers/firecrawl/adapter.ts
15
- import FirecrawlClient from "@mendable/firecrawl-js";
16
16
  var FIRECRAWL_CLOUD_HOST = "api.firecrawl.dev";
17
17
  var FIRECRAWL_DEFAULT_API_URL = "https://api.firecrawl.dev";
18
18
  var FIRECRAWL_QUESTION_LIMIT = 1e4;
19
19
  var firecrawlImplementation = {
20
- async search(query, maxResults, config, _context, options) {
21
- const client = createClient(config);
22
- const response = await client.search(query, {
23
- ...options ?? {},
24
- limit: maxResults
25
- });
20
+ async search(query, maxResults, config, context, options) {
21
+ if (!query.trim()) {
22
+ throw new Error("Query cannot be empty");
23
+ }
24
+ const response = await firecrawlRequest(
25
+ config,
26
+ "/v2/search",
27
+ { ...options ?? {}, query, limit: maxResults },
28
+ context.signal,
29
+ "Firecrawl search failed."
30
+ );
26
31
  return {
27
32
  provider: "firecrawl",
28
- results: flattenSearchResults(response).slice(0, maxResults)
33
+ results: flattenSearchResults(
34
+ asRecord(response.data) ?? {}
35
+ ).slice(0, maxResults)
29
36
  };
30
37
  },
31
- async contents(urls, config, _context, options) {
32
- const client = createClient(config);
38
+ async contents(urls, config, context, options) {
39
+ const { autoResume: _autoResume, ...callOptions } = options ?? {};
33
40
  const scrapeOptions = {
34
41
  formats: ["markdown"],
35
42
  onlyMainContent: true,
36
- ...options ?? {},
37
- autoResume: false
43
+ ...callOptions
38
44
  };
39
45
  return {
40
46
  provider: "firecrawl",
41
47
  answers: await Promise.all(
42
48
  urls.map(async (url) => {
43
49
  try {
44
- const document = await client.scrape(url, scrapeOptions);
50
+ const response = await firecrawlRequest(
51
+ config,
52
+ "/v2/scrape",
53
+ { ...scrapeOptions, url: url.trim() },
54
+ context.signal,
55
+ "Firecrawl scrape failed."
56
+ );
57
+ const document = asRecord(response.data) ?? {};
45
58
  const content = getDocumentContent(document);
46
59
  return content ? {
47
60
  url,
48
61
  content,
49
- ...document.metadata ? {
62
+ ...asRecord(document.metadata) ? {
50
63
  metadata: document.metadata
51
64
  } : {}
52
65
  } : {
@@ -94,22 +107,6 @@ var firecrawlImplementation = {
94
107
  };
95
108
  }
96
109
  };
97
- function createClient(config) {
98
- const apiUrl = config.baseUrl;
99
- const apiKey = config.credentials?.api;
100
- if (isFirecrawlCloudApiUrl(apiUrl) && !apiKey) {
101
- throw new WebfoxError(
102
- "PROVIDER_UNAVAILABLE",
103
- "Firecrawl cloud requires FIRECRAWL_API_KEY; self-hosted instances can use baseUrl without a key."
104
- );
105
- }
106
- return new FirecrawlClient({
107
- // Firecrawl counts attempts, unlike SDKs whose maxRetries excludes the first call.
108
- maxRetries: 1,
109
- apiKey,
110
- apiUrl
111
- });
112
- }
113
110
  function isFirecrawlCloudApiUrl(apiUrl) {
114
111
  return !apiUrl || apiUrl.includes(FIRECRAWL_CLOUD_HOST);
115
112
  }
@@ -136,6 +133,19 @@ function stripAnswerOnlyOptions(options) {
136
133
  return scrapeOptions;
137
134
  }
138
135
  async function scrapeQuestion(config, url, question, options, signal) {
136
+ return await firecrawlRequest(
137
+ config,
138
+ "/v2/scrape",
139
+ {
140
+ ...options,
141
+ url,
142
+ formats: [{ type: "question", question }]
143
+ },
144
+ signal,
145
+ "Firecrawl scrape failed."
146
+ );
147
+ }
148
+ async function firecrawlRequest(config, path, body, signal, failure) {
139
149
  const apiUrl = config.baseUrl ?? FIRECRAWL_DEFAULT_API_URL;
140
150
  const apiKey = config.credentials?.api;
141
151
  if (isFirecrawlCloudApiUrl(apiUrl) && !apiKey) {
@@ -144,45 +154,28 @@ async function scrapeQuestion(config, url, question, options, signal) {
144
154
  "Firecrawl cloud requires FIRECRAWL_API_KEY; self-hosted instances can use baseUrl without a key."
145
155
  );
146
156
  }
147
- const response = await fetch(joinUrl(apiUrl, "/v2/scrape"), {
148
- signal,
149
- method: "POST",
150
- headers: {
151
- "Content-Type": "application/json",
152
- ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {}
157
+ const payload = await requestJson(
158
+ joinUrl(apiUrl, path),
159
+ {
160
+ // Omit Authorization without a key: Firecrawl grants keyless access
161
+ // only when the header is absent.
162
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
163
+ body,
164
+ signal
153
165
  },
154
- body: JSON.stringify({
155
- ...options,
156
- url,
157
- formats: [{ type: "question", question }]
158
- })
159
- });
160
- const payload = await readJsonResponse(response);
161
- if (!response.ok) {
162
- throw httpError(response, readFirecrawlError(payload, response.statusText));
163
- }
166
+ (value, response) => readFirecrawlError(value, response.statusText || failure)
167
+ );
164
168
  if (isFirecrawlFailure(payload)) {
165
- throw new Error(readFirecrawlError(payload, "Firecrawl scrape failed."));
169
+ throw new Error(readFirecrawlError(payload, failure));
166
170
  }
167
- return payload;
168
- }
169
- function joinUrl(baseUrl, path) {
170
- return `${baseUrl.replace(/\/+$/g, "")}/${path.replace(/^\/+/g, "")}`;
171
- }
172
- async function readJsonResponse(response) {
173
- const text = await response.text();
174
- if (!text) {
175
- return {};
176
- }
177
- try {
178
- return JSON.parse(text);
179
- } catch {
180
- return text;
171
+ const record = asRecord(payload);
172
+ if (!record) {
173
+ throw new Error(`Unexpected Firecrawl response: ${formatJson(payload)}`);
181
174
  }
175
+ return record;
182
176
  }
183
177
  function isFirecrawlFailure(value) {
184
- const record = asRecord(value);
185
- return record?.success === false || record?.error !== void 0;
178
+ return asRecord(value)?.success !== true;
186
179
  }
187
180
  function readFirecrawlError(value, fallback) {
188
181
  const record = asRecord(value);
@@ -201,7 +194,12 @@ function getFirecrawlDocument(value) {
201
194
  }
202
195
  function flattenSearchResults(response) {
203
196
  return ["web", "news", "images"].flatMap(
204
- (source) => (response[source] ?? []).map((entry) => toSearchResult(source, entry)).filter((entry) => entry !== null)
197
+ (source) => (response[source] ?? []).map(
198
+ (entry) => toSearchResult(
199
+ source,
200
+ typeof entry === "string" ? { url: entry } : entry
201
+ )
202
+ ).filter((entry) => entry !== null)
205
203
  );
206
204
  }
207
205
  function toSearchResult(source, value) {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  trimSnippet
3
- } from "./chunk-X6ZBIHZC.js";
3
+ } from "./chunk-FSTCLBSF.js";
4
+ import "./chunk-U4BLULLV.js";
4
5
 
5
6
  // src/providers/perplexity/adapter.ts
6
7
  import PerplexityClient from "@perplexity-ai/perplexity_ai";
@@ -2,10 +2,10 @@ import {
2
2
  SERPBASE_MODES,
3
3
  serpbaseProvider,
4
4
  validateOptions
5
- } from "./chunk-QERSNL3J.js";
5
+ } from "./chunk-LTJOWNI2.js";
6
6
  import {
7
7
  trimSnippet
8
- } from "./chunk-X6ZBIHZC.js";
8
+ } from "./chunk-FSTCLBSF.js";
9
9
  import "./chunk-GPACLYNT.js";
10
10
  import {
11
11
  WebfoxError,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  trimSnippet
3
- } from "./chunk-X6ZBIHZC.js";
3
+ } from "./chunk-FSTCLBSF.js";
4
4
  import {
5
5
  orderedContents
6
6
  } from "./chunk-YZXM4U2C.js";
@@ -5,7 +5,7 @@ import "./chunk-I2UIACY3.js";
5
5
  import "./chunk-WCTOYZVB.js";
6
6
  import {
7
7
  trimSnippet
8
- } from "./chunk-X6ZBIHZC.js";
8
+ } from "./chunk-FSTCLBSF.js";
9
9
  import "./chunk-U4BLULLV.js";
10
10
 
11
11
  // src/providers/openai/adapter.ts
@@ -0,0 +1,76 @@
1
+ import {
2
+ WebfoxError,
3
+ httpError
4
+ } from "./chunk-U4BLULLV.js";
5
+
6
+ // src/providers/shared.ts
7
+ function trimSnippet(input, maxLength = 300) {
8
+ const text = (input ?? "").replace(/\s+/g, " ").trim();
9
+ return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}\u2026`;
10
+ }
11
+ function normalizeContentText(input) {
12
+ return (input ?? "").replace(/\r/g, "").trim().split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n").replace(/\n{3,}/g, "\n\n");
13
+ }
14
+ function asJsonObject(value) {
15
+ return value ? { ...value } : {};
16
+ }
17
+ function formatJson(value) {
18
+ return JSON.stringify(value, null, 2);
19
+ }
20
+ function joinUrl(baseUrl, path) {
21
+ return `${baseUrl.replace(/\/+$/g, "")}/${path.replace(/^\/+/g, "")}`;
22
+ }
23
+ async function readResponseBody(response) {
24
+ const text = await response.text();
25
+ if (!text) return void 0;
26
+ try {
27
+ return JSON.parse(text);
28
+ } catch {
29
+ return text;
30
+ }
31
+ }
32
+ function expectJsonBody(payload, response) {
33
+ if (payload === void 0) {
34
+ throw new WebfoxError(
35
+ "PROVIDER_FAILURE",
36
+ `Expected a JSON response but received an empty body (status ${response.status}).`
37
+ );
38
+ }
39
+ if (typeof payload === "string") {
40
+ const snippet = payload.trim().replace(/\s+/g, " ").slice(0, 200);
41
+ throw new WebfoxError(
42
+ "PROVIDER_FAILURE",
43
+ `Expected a JSON response but received a non-JSON body (status ${response.status}): ${snippet}`
44
+ );
45
+ }
46
+ return payload;
47
+ }
48
+ async function requestJson(url, init, describeError) {
49
+ const method = init.method ?? (init.body === void 0 ? "GET" : "POST");
50
+ const response = await fetch(url, {
51
+ method,
52
+ headers: {
53
+ Accept: "application/json",
54
+ ...init.body === void 0 ? {} : { "Content-Type": "application/json" },
55
+ ...init.headers
56
+ },
57
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) },
58
+ signal: init.signal
59
+ });
60
+ const payload = await readResponseBody(response);
61
+ if (!response.ok) {
62
+ throw httpError(response, describeError(payload, response));
63
+ }
64
+ return expectJsonBody(payload, response);
65
+ }
66
+
67
+ export {
68
+ trimSnippet,
69
+ normalizeContentText,
70
+ asJsonObject,
71
+ formatJson,
72
+ joinUrl,
73
+ readResponseBody,
74
+ expectJsonBody,
75
+ requestJson
76
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  optionSchema,
3
3
  providers
4
- } from "./chunk-QERSNL3J.js";
4
+ } from "./chunk-LTJOWNI2.js";
5
5
  import {
6
6
  CAPABILITIES
7
7
  } from "./chunk-GPACLYNT.js";
@@ -9,7 +9,7 @@ import {
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "webfox",
12
- version: "4.8.0",
12
+ version: "4.9.0",
13
13
  description: "Search the web, extract pages, get grounded answers, and run deep research with the web CLI, a TypeScript library, and a pi extension. Bring your own providers and API keys.",
14
14
  type: "module",
15
15
  files: [
@@ -101,17 +101,14 @@ var package_default = {
101
101
  "test:watch": "vitest"
102
102
  },
103
103
  dependencies: {
104
- "@mendable/firecrawl-js": "^4.38.0",
105
- "@perplexity-ai/perplexity_ai": "^0.38.5",
106
- "@tavily/core": "^0.7.9",
107
- cloudflare: "^7.1.0",
104
+ "@perplexity-ai/perplexity_ai": "^0.41.0",
105
+ "@tavily/core": "^0.7.14",
106
+ cloudflare: "^7.3.0",
108
107
  commander: "^14.0.3",
109
- "exa-js": "^2.19.0",
110
- "linkup-sdk": "^3.6.0",
111
- openai: "^7.10.0",
112
- "parallel-web": "^1.3.3",
108
+ openai: "^7.47.0",
109
+ "parallel-web": "^1.3.7",
113
110
  picocolors: "^1.1.1",
114
- "valyu-js": "^2.10.1",
111
+ "valyu-js": "^2.11.5",
115
112
  yaml: "^2.9.0"
116
113
  },
117
114
  devDependencies: {
@@ -504,7 +504,7 @@ var braveProvider = defineProvider({
504
504
  retrySafe: false
505
505
  }
506
506
  },
507
- load: async () => (await import("./adapter-67TIISDW.js")).adapter
507
+ load: async () => (await import("./adapter-A5WYGGZI.js")).adapter
508
508
  });
509
509
 
510
510
  // src/providers/cloudflare/definition.ts
@@ -944,7 +944,7 @@ var exaProvider = defineProvider({
944
944
  retrySafe: false
945
945
  }
946
946
  },
947
- load: async () => (await import("./adapter-PADXKW43.js")).adapter
947
+ load: async () => (await import("./adapter-HRI5ZMGK.js")).adapter
948
948
  });
949
949
 
950
950
  // src/providers/firecrawl/definition.ts
@@ -1150,7 +1150,7 @@ var firecrawlProvider = defineProvider({
1150
1150
  retrySafe: false
1151
1151
  }
1152
1152
  },
1153
- load: async () => (await import("./adapter-XBAQSK7I.js")).adapter
1153
+ load: async () => (await import("./adapter-L6BFKWGX.js")).adapter
1154
1154
  });
1155
1155
 
1156
1156
  // src/providers/gemini/definition.ts
@@ -1473,7 +1473,7 @@ var linkupProvider = defineProvider({
1473
1473
  retrySafe: false
1474
1474
  }
1475
1475
  },
1476
- load: async () => (await import("./adapter-HPTMLH75.js")).adapter
1476
+ load: async () => (await import("./adapter-K6AMSUCR.js")).adapter
1477
1477
  });
1478
1478
 
1479
1479
  // src/providers/ollama/definition.ts
@@ -1502,7 +1502,7 @@ var ollamaProvider = defineProvider({
1502
1502
  retrySafe: true
1503
1503
  }
1504
1504
  },
1505
- load: async () => (await import("./adapter-ISZQTZDP.js")).adapter
1505
+ load: async () => (await import("./adapter-HTMXAJTU.js")).adapter
1506
1506
  });
1507
1507
 
1508
1508
  // src/providers/openai/definition.ts
@@ -1610,7 +1610,7 @@ var openaiProvider = defineProvider({
1610
1610
  retrySafe: false
1611
1611
  }
1612
1612
  },
1613
- load: async () => (await import("./adapter-HITGYUXN.js")).adapter
1613
+ load: async () => (await import("./adapter-VMMS4YQG.js")).adapter
1614
1614
  });
1615
1615
 
1616
1616
  // src/providers/parallel/definition.ts
@@ -1769,7 +1769,7 @@ var parallelProvider = defineProvider({
1769
1769
  retrySafe: true
1770
1770
  }
1771
1771
  },
1772
- load: async () => (await import("./adapter-2YBFCBCZ.js")).adapter
1772
+ load: async () => (await import("./adapter-G3I7JEO5.js")).adapter
1773
1773
  });
1774
1774
 
1775
1775
  // src/providers/perplexity/definition.ts
@@ -1890,7 +1890,7 @@ var perplexityProvider = defineProvider({
1890
1890
  answer: { options: chatOptions, retrySafe: false },
1891
1891
  research: { options: chatOptions, retrySafe: false }
1892
1892
  },
1893
- load: async () => (await import("./adapter-SLTIAAML.js")).adapter
1893
+ load: async () => (await import("./adapter-OK2PTP6R.js")).adapter
1894
1894
  });
1895
1895
 
1896
1896
  // src/providers/serpbase/definition.ts
@@ -1991,7 +1991,7 @@ var serpbaseProvider = defineProvider({
1991
1991
  retrySafe: true
1992
1992
  }
1993
1993
  },
1994
- load: async () => (await import("./adapter-QU2G7O25.js")).adapter
1994
+ load: async () => (await import("./adapter-QIP32KA4.js")).adapter
1995
1995
  });
1996
1996
 
1997
1997
  // src/providers/serper/definition.ts
@@ -2131,7 +2131,7 @@ var serperProvider = defineProvider({
2131
2131
  retrySafe: true
2132
2132
  }
2133
2133
  },
2134
- load: async () => (await import("./adapter-I6OUKBZQ.js")).adapter
2134
+ load: async () => (await import("./adapter-C2VZHFUB.js")).adapter
2135
2135
  });
2136
2136
 
2137
2137
  // src/providers/tavily/definition.ts
@@ -2326,7 +2326,7 @@ var tavilyProvider = defineProvider({
2326
2326
  retrySafe: true
2327
2327
  }
2328
2328
  },
2329
- load: async () => (await import("./adapter-SDQX2533.js")).adapter
2329
+ load: async () => (await import("./adapter-UIFBJKKF.js")).adapter
2330
2330
  });
2331
2331
 
2332
2332
  // src/providers/valyu/definition.ts
@@ -2794,7 +2794,7 @@ var valyuProvider = defineProvider({
2794
2794
  retrySafe: false
2795
2795
  }
2796
2796
  },
2797
- load: async () => (await import("./adapter-UAZNLDSS.js")).adapter
2797
+ load: async () => (await import("./adapter-764OQFKY.js")).adapter
2798
2798
  });
2799
2799
 
2800
2800
  // src/providers/youcom/definition.ts
@@ -2985,7 +2985,7 @@ var youcomProvider = defineProvider({
2985
2985
  retrySafe: true
2986
2986
  }
2987
2987
  },
2988
- load: async () => (await import("./adapter-YOHLAGYM.js")).adapter
2988
+ load: async () => (await import("./adapter-2UVAARTV.js")).adapter
2989
2989
  });
2990
2990
 
2991
2991
  // src/providers/registry.ts
@@ -6,14 +6,14 @@ import {
6
6
  } from "./chunk-WCTOYZVB.js";
7
7
  import {
8
8
  configurationSchema
9
- } from "./chunk-G7LMBPW4.js";
9
+ } from "./chunk-HCYQF24V.js";
10
10
  import {
11
11
  effectiveOptions,
12
12
  optionSchema,
13
13
  providers,
14
14
  selectProvider,
15
15
  validateConfiguredOptions
16
- } from "./chunk-QERSNL3J.js";
16
+ } from "./chunk-LTJOWNI2.js";
17
17
  import {
18
18
  cancelProcessGroup,
19
19
  killProcessGroup
package/dist/cli.js CHANGED
@@ -11,14 +11,14 @@ import {
11
11
  redactConfig,
12
12
  resolveConfigPath,
13
13
  setCapabilityDefault
14
- } from "./chunk-IRKSK34I.js";
14
+ } from "./chunk-QZFN2747.js";
15
15
  import "./chunk-WCTOYZVB.js";
16
16
  import {
17
17
  PACKAGE_VERSION
18
- } from "./chunk-G7LMBPW4.js";
18
+ } from "./chunk-HCYQF24V.js";
19
19
  import {
20
20
  validateConfiguredOptions
21
- } from "./chunk-QERSNL3J.js";
21
+ } from "./chunk-LTJOWNI2.js";
22
22
  import "./chunk-ZVTVERW5.js";
23
23
  import {
24
24
  CAPABILITIES,
package/dist/durable.js CHANGED
@@ -8,10 +8,10 @@ import {
8
8
  } from "./chunk-BTWECET5.js";
9
9
  import {
10
10
  createWebfox
11
- } from "./chunk-IRKSK34I.js";
11
+ } from "./chunk-QZFN2747.js";
12
12
  import "./chunk-WCTOYZVB.js";
13
- import "./chunk-G7LMBPW4.js";
14
- import "./chunk-QERSNL3J.js";
13
+ import "./chunk-HCYQF24V.js";
14
+ import "./chunk-LTJOWNI2.js";
15
15
  import "./chunk-ZVTVERW5.js";
16
16
  import {
17
17
  CAPABILITIES
package/dist/index.js CHANGED
@@ -6,14 +6,14 @@ import {
6
6
  resolveConfigPath,
7
7
  setCapabilityDefault,
8
8
  validateConfig
9
- } from "./chunk-IRKSK34I.js";
9
+ } from "./chunk-QZFN2747.js";
10
10
  import "./chunk-WCTOYZVB.js";
11
11
  import {
12
12
  CONFIG_SCHEMA_URL
13
- } from "./chunk-G7LMBPW4.js";
13
+ } from "./chunk-HCYQF24V.js";
14
14
  import {
15
15
  validateConfiguredOptions
16
- } from "./chunk-QERSNL3J.js";
16
+ } from "./chunk-LTJOWNI2.js";
17
17
  import "./chunk-ZVTVERW5.js";
18
18
  import {
19
19
  CAPABILITIES,
package/dist/pi.js CHANGED
@@ -9,10 +9,10 @@ import {
9
9
  import {
10
10
  createWebfox,
11
11
  resolveConfigPath
12
- } from "./chunk-IRKSK34I.js";
12
+ } from "./chunk-QZFN2747.js";
13
13
  import "./chunk-WCTOYZVB.js";
14
- import "./chunk-G7LMBPW4.js";
15
- import "./chunk-QERSNL3J.js";
14
+ import "./chunk-HCYQF24V.js";
15
+ import "./chunk-LTJOWNI2.js";
16
16
  import "./chunk-ZVTVERW5.js";
17
17
  import {
18
18
  CAPABILITIES,
@@ -2,3 +2,19 @@ export declare function trimSnippet(input: string | undefined, maxLength?: numbe
2
2
  export declare function normalizeContentText(input: string | undefined): string;
3
3
  export declare function asJsonObject(value: Record<string, unknown> | undefined): Record<string, unknown>;
4
4
  export declare function formatJson(value: unknown): string;
5
+ export declare function joinUrl(baseUrl: string, path: string): string;
6
+ /** Read a response body as JSON, falling back to its text. */
7
+ export declare function readResponseBody(response: Response): Promise<unknown>;
8
+ /** Reject successful responses whose body is empty or not JSON. */
9
+ export declare function expectJsonBody(payload: unknown, response: Response): unknown;
10
+ /**
11
+ * Send one JSON request without retries; the runtime owns retry policy.
12
+ * Non-2xx responses become classified provider errors with the message that
13
+ * `describeError` extracts from the response body.
14
+ */
15
+ export declare function requestJson(url: string, init: {
16
+ method?: "GET" | "POST";
17
+ headers?: Record<string, string>;
18
+ body?: unknown;
19
+ signal?: AbortSignal;
20
+ }, describeError: (payload: unknown, response: Response) => string): Promise<unknown>;
package/docs/provider.md CHANGED
@@ -160,8 +160,49 @@ web search --provider openai --help
160
160
 
161
161
  Supports search and page extraction. Set `PARALLEL_API_KEY`.
162
162
 
163
+ Search defaults to `advanced`. Choose `fast` for interactive lookups, or see
164
+ [Parallel's search modes](https://docs.parallel.ai/search/modes) for the available
165
+ tradeoffs:
166
+
167
+ ```sh
168
+ web search "Node.js release notes" --provider parallel --mode fast
169
+ ```
170
+
171
+ Limit a query to particular sources with `advanced_settings.source_policy`.
172
+ Use Webfox's `--max-results` to control the number of results:
173
+
174
+ ```sh
175
+ web search "Node.js release notes" --provider parallel --mode fast --max-results 3 \
176
+ --options-json '{"advanced_settings":{"source_policy":{"include_domains":["nodejs.org"]}}}'
177
+ ```
178
+
179
+ To reuse Fast mode, merge this into your [configuration](./reference.md#configuration):
180
+
181
+ ```yaml
182
+ providers:
183
+ parallel:
184
+ options:
185
+ search:
186
+ mode: fast
187
+ ```
188
+
189
+ Provider options don't select a provider. Continue to pass `--provider parallel`
190
+ or use your saved Parallel search default. A request's `--mode` overrides the
191
+ saved mode.
192
+
193
+ For known URLs, use page extraction instead of another search. Parallel returns
194
+ full page content by default:
195
+
196
+ ```sh
197
+ web contents https://nodejs.org/en/about/previous-releases --provider parallel
198
+ ```
199
+
200
+ To request only excerpts relevant to a question, disable full content and enable
201
+ excerpts with an objective:
202
+
163
203
  ```sh
164
- web search "Node.js release notes" --provider parallel
204
+ web contents https://nodejs.org/en/about/previous-releases --provider parallel \
205
+ --no-full-content --excerpts --objective "Which Node.js releases are supported?"
165
206
  ```
166
207
 
167
208
  ## Perplexity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webfox",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "Search the web, extract pages, get grounded answers, and run deep research with the web CLI, a TypeScript library, and a pi extension. Bring your own providers and API keys.",
5
5
  "type": "module",
6
6
  "files": [
@@ -92,17 +92,14 @@
92
92
  "test:watch": "vitest"
93
93
  },
94
94
  "dependencies": {
95
- "@mendable/firecrawl-js": "^4.38.0",
96
- "@perplexity-ai/perplexity_ai": "^0.38.5",
97
- "@tavily/core": "^0.7.9",
98
- "cloudflare": "^7.1.0",
95
+ "@perplexity-ai/perplexity_ai": "^0.41.0",
96
+ "@tavily/core": "^0.7.14",
97
+ "cloudflare": "^7.3.0",
99
98
  "commander": "^14.0.3",
100
- "exa-js": "^2.19.0",
101
- "linkup-sdk": "^3.6.0",
102
- "openai": "^7.10.0",
103
- "parallel-web": "^1.3.3",
99
+ "openai": "^7.47.0",
100
+ "parallel-web": "^1.3.7",
104
101
  "picocolors": "^1.1.1",
105
- "valyu-js": "^2.10.1",
102
+ "valyu-js": "^2.11.5",
106
103
  "yaml": "^2.9.0"
107
104
  },
108
105
  "devDependencies": {
@@ -1,21 +0,0 @@
1
- // src/providers/shared.ts
2
- function trimSnippet(input, maxLength = 300) {
3
- const text = (input ?? "").replace(/\s+/g, " ").trim();
4
- return text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}\u2026`;
5
- }
6
- function normalizeContentText(input) {
7
- return (input ?? "").replace(/\r/g, "").trim().split("\n").map((line) => line.replace(/[ \t]+$/g, "")).join("\n").replace(/\n{3,}/g, "\n\n");
8
- }
9
- function asJsonObject(value) {
10
- return value ? { ...value } : {};
11
- }
12
- function formatJson(value) {
13
- return JSON.stringify(value, null, 2);
14
- }
15
-
16
- export {
17
- trimSnippet,
18
- normalizeContentText,
19
- asJsonObject,
20
- formatJson
21
- };