webfox 4.9.0 → 4.11.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.
Files changed (47) hide show
  1. package/README.md +4 -12
  2. package/dist/{adapter-UIFBJKKF.js → adapter-44ZBIK47.js} +4 -6
  3. package/dist/{adapter-HTMXAJTU.js → adapter-57MELLYI.js} +2 -8
  4. package/dist/{adapter-VMMS4YQG.js → adapter-BZO5UEI6.js} +3 -15
  5. package/dist/{adapter-OK2PTP6R.js → adapter-D4EXZEDL.js} +16 -48
  6. package/dist/{adapter-G3I7JEO5.js → adapter-DJCTEEOA.js} +3 -5
  7. package/dist/{adapter-764OQFKY.js → adapter-DLNMW34W.js} +8 -35
  8. package/dist/{adapter-HRI5ZMGK.js → adapter-GFXYSHKP.js} +9 -43
  9. package/dist/{adapter-2UVAARTV.js → adapter-GJOUFE2U.js} +1 -1
  10. package/dist/{adapter-C2VZHFUB.js → adapter-MZ7USB6Q.js} +3 -4
  11. package/dist/adapter-ODDFBGJG.js +167 -0
  12. package/dist/{adapter-K6AMSUCR.js → adapter-QQZFHUOP.js} +14 -37
  13. package/dist/{adapter-AUYLTDPM.js → adapter-RR55EZAW.js} +1 -1
  14. package/dist/adapter-SZMVKAMC.js +91 -0
  15. package/dist/{adapter-6EZAIE65.js → adapter-VXT4C6RZ.js} +16 -66
  16. package/dist/{adapter-L6BFKWGX.js → adapter-WXEVTCM3.js} +7 -14
  17. package/dist/{adapter-VW4FMMJC.js → adapter-XSQX3GAI.js} +13 -5
  18. package/dist/{adapter-QIP32KA4.js → adapter-XU2APKPC.js} +3 -3
  19. package/dist/{adapter-A5WYGGZI.js → adapter-YVZ4YBK5.js} +8 -16
  20. package/dist/chunk-6RQGO727.js +71 -0
  21. package/dist/{chunk-HCYQF24V.js → chunk-KMUCUJVN.js} +3 -3
  22. package/dist/{chunk-EF4VMDZL.js → chunk-LDVDVA4Z.js} +1 -4
  23. package/dist/{chunk-GPACLYNT.js → chunk-NENL6L6N.js} +2 -0
  24. package/dist/{chunk-LTJOWNI2.js → chunk-PVNWACMT.js} +199 -17
  25. package/dist/{chunk-YZXM4U2C.js → chunk-TYJ6RHKB.js} +10 -0
  26. package/dist/{chunk-QZFN2747.js → chunk-YILOS4E7.js} +23 -9
  27. package/dist/cli.js +5 -5
  28. package/dist/config.schema.json +299 -0
  29. package/dist/contents.d.ts +5 -0
  30. package/dist/document-schema.d.ts +17 -5
  31. package/dist/domain.d.ts +14 -2
  32. package/dist/durable.js +5 -5
  33. package/dist/index.js +4 -4
  34. package/dist/pi.js +33 -14
  35. package/dist/providers/contract.d.ts +12 -1
  36. package/dist/providers/keenable/adapter.d.ts +5 -0
  37. package/dist/providers/keenable/definition.d.ts +1 -0
  38. package/dist/providers/keenable/types.d.ts +4 -0
  39. package/dist/providers/registry.d.ts +2 -0
  40. package/dist/providers/sofya/adapter.d.ts +8 -0
  41. package/dist/providers/sofya/definition.d.ts +1 -0
  42. package/dist/providers/sofya/types.d.ts +4 -0
  43. package/dist/render.d.ts +8 -2
  44. package/docs/provider.md +44 -0
  45. package/examples/custom/README.md +9 -3
  46. package/package.json +1 -1
  47. package/dist/chunk-BTWECET5.js +0 -31
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  executeAsyncResearch
3
- } from "./chunk-EF4VMDZL.js";
3
+ } from "./chunk-LDVDVA4Z.js";
4
4
  import "./chunk-I2UIACY3.js";
5
5
  import "./chunk-WCTOYZVB.js";
6
6
  import {
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-FSTCLBSF.js";
13
13
  import {
14
14
  orderedContents
15
- } from "./chunk-YZXM4U2C.js";
15
+ } from "./chunk-TYJ6RHKB.js";
16
16
  import {
17
17
  WebfoxError,
18
18
  asWebfoxError
@@ -54,13 +54,10 @@ var linkupImplementation = {
54
54
  ].includes(key) && value !== void 0
55
55
  )
56
56
  );
57
- return response.markdown ? {
57
+ return {
58
58
  url,
59
- content: response.markdown,
59
+ ...response.markdown ? { content: response.markdown } : {},
60
60
  ...Object.keys(metadata).length ? { metadata } : {}
61
- } : {
62
- url,
63
- error: "No content returned for this URL."
64
61
  };
65
62
  } catch (error) {
66
63
  return {
@@ -251,37 +248,17 @@ function linkupError(response, payload) {
251
248
  }
252
249
  function formatResearchTaskOutput(task) {
253
250
  const output = task.output;
254
- if (!output) {
251
+ if (output == null) return { provider: "linkup", text: "" };
252
+ if (typeof output === "string") return { provider: "linkup", text: output };
253
+ const outputType = readString(asRecord(task.input)?.outputType);
254
+ const record = asRecord(output);
255
+ if (outputType !== "structured" && record && ("answer" in record || "sources" in record))
255
256
  return {
256
257
  provider: "linkup",
257
- text: "Linkup research completed without textual output."
258
+ text: readString(record.answer) ?? "",
259
+ sources: readSources(record.sources)
258
260
  };
259
- }
260
- const outputRecord = asRecord(output);
261
- const inputRecord = asRecord(task.input);
262
- const outputType = inputRecord ? readString(inputRecord.outputType) : void 0;
263
- const answer = outputRecord ? readString(outputRecord.answer) : void 0;
264
- const sources = outputRecord ? readSources(outputRecord.sources) : [];
265
- if (outputType !== "structured" && answer !== void 0) {
266
- const lines = [answer];
267
- if (sources.length > 0) {
268
- lines.push("");
269
- lines.push("Sources:");
270
- for (const [index, source] of sources.entries()) {
271
- lines.push(`${index + 1}. ${source.title}`);
272
- lines.push(` ${source.url}`);
273
- }
274
- }
275
- return {
276
- provider: "linkup",
277
- text: lines.join("\n").trimEnd(),
278
- itemCount: sources.length
279
- };
280
- }
281
- return {
282
- provider: "linkup",
283
- text: formatJson(output)
284
- };
261
+ return { provider: "linkup", text: formatJson(output) };
285
262
  }
286
263
  function toSearchResult(value) {
287
264
  const entry = asRecord(value);
@@ -289,7 +266,7 @@ function toSearchResult(value) {
289
266
  return null;
290
267
  }
291
268
  const url = readString(entry.url) ?? "";
292
- const title = readString(entry.name) ?? (url || "Untitled");
269
+ const title = readString(entry.name);
293
270
  const type = readString(entry.type);
294
271
  const favicon = readString(entry.favicon);
295
272
  const snippet = type === "text" ? trimSnippet(readString(entry.content) ?? "") : "";
@@ -319,7 +296,7 @@ function readSources(value) {
319
296
  }
320
297
  return [
321
298
  {
322
- title: readString(source.name) ?? url,
299
+ title: readString(source.name),
323
300
  url
324
301
  }
325
302
  ];
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  orderedContents
3
- } from "./chunk-YZXM4U2C.js";
3
+ } from "./chunk-TYJ6RHKB.js";
4
4
  import {
5
5
  asWebfoxError
6
6
  } from "./chunk-U4BLULLV.js";
@@ -0,0 +1,91 @@
1
+ import {
2
+ joinUrl,
3
+ requestJson,
4
+ trimSnippet
5
+ } from "./chunk-FSTCLBSF.js";
6
+ import {
7
+ WebfoxError
8
+ } from "./chunk-U4BLULLV.js";
9
+
10
+ // src/providers/keenable/adapter.ts
11
+ var DEFAULT_BASE_URL = "https://api.keenable.ai";
12
+ var MAX_RESULTS = 50;
13
+ var FORWARDED_OPTIONS = [
14
+ "mode",
15
+ "site",
16
+ "published_after",
17
+ "published_before",
18
+ "acquired_after",
19
+ "acquired_before",
20
+ "query_time",
21
+ "snippet_max_length"
22
+ ];
23
+ var adapter = {
24
+ async search(request, config, context) {
25
+ const apiKey = config.credentials?.api;
26
+ if (!apiKey) throw new Error("Keenable search is missing an API key");
27
+ const maxResults = Math.max(
28
+ 1,
29
+ Math.min(MAX_RESULTS, Math.trunc(request.maxResults || 0))
30
+ );
31
+ const options = asRecord(request.options) ?? {};
32
+ const payload = asRecord(
33
+ await requestJson(
34
+ joinUrl(config.baseUrl ?? DEFAULT_BASE_URL, "v1/search"),
35
+ {
36
+ method: "POST",
37
+ headers: { "X-API-Key": apiKey, "X-Keenable-Title": "webfox" },
38
+ body: {
39
+ query: request.query,
40
+ max_results: maxResults,
41
+ ...Object.fromEntries(
42
+ FORWARDED_OPTIONS.flatMap(
43
+ (key) => options[key] === void 0 ? [] : [[key, options[key]]]
44
+ )
45
+ )
46
+ },
47
+ signal: context.signal
48
+ },
49
+ describeError
50
+ )
51
+ );
52
+ const entries = payload?.results;
53
+ if (!payload || !Array.isArray(entries)) {
54
+ throw new WebfoxError(
55
+ "PROVIDER_FAILURE",
56
+ "Keenable returned an invalid search response.",
57
+ { retryable: false }
58
+ );
59
+ }
60
+ const results = entries.map(toSearchResult).filter((result) => result !== void 0);
61
+ return { provider: "keenable", results: results.slice(0, maxResults) };
62
+ }
63
+ };
64
+ function describeError(payload, response) {
65
+ const record = asRecord(payload);
66
+ const detail = text(record?.detail) ?? text(record?.error) ?? text(record?.message) ?? (typeof payload === "string" ? text(payload) : void 0) ?? (record ? JSON.stringify(record) : void 0);
67
+ const trimmed = detail?.trim().slice(0, 500);
68
+ return `Keenable search failed (HTTP ${response.status})${trimmed ? `: ${trimmed}` : "."}`;
69
+ }
70
+ function toSearchResult(entry) {
71
+ const record = asRecord(entry);
72
+ const url = text(record?.url);
73
+ if (!record || !url) return void 0;
74
+ const { title, url: _url, description, snippet, ...metadata } = record;
75
+ const excerpt = text(snippet) ?? text(description);
76
+ return {
77
+ title: text(title),
78
+ url,
79
+ snippet: excerpt ? trimSnippet(excerpt) : "",
80
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
81
+ };
82
+ }
83
+ function asRecord(value) {
84
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
85
+ }
86
+ function text(value) {
87
+ return typeof value === "string" && value.trim() ? value : void 0;
88
+ }
89
+ export {
90
+ adapter
91
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  executeAsyncResearch
3
- } from "./chunk-EF4VMDZL.js";
3
+ } from "./chunk-LDVDVA4Z.js";
4
4
  import "./chunk-I2UIACY3.js";
5
5
  import "./chunk-WCTOYZVB.js";
6
6
  import {
@@ -29,24 +29,12 @@ var geminiImplementation = {
29
29
  const text = Array.isArray(parts) ? parts.filter(
30
30
  (part) => isPlainObject(part) && !part.thought && typeof part.text === "string"
31
31
  ).map((part) => part.text).join("").trim() : "";
32
- const lines = [text || "No answer returned."];
33
- const sources = extractGroundingSources(
34
- asRecord(candidate.groundingMetadata).groundingChunks
35
- );
36
- if (sources.length > 0) {
37
- lines.push("");
38
- lines.push("Sources:");
39
- for (const [index, source] of sources.entries()) {
40
- lines.push(`${index + 1}. ${source.title}`);
41
- if (source.url) {
42
- lines.push(` ${source.url}`);
43
- }
44
- }
45
- }
46
32
  return {
47
33
  provider: "gemini",
48
- text: lines.join("\n").trimEnd(),
49
- itemCount: sources.length
34
+ text,
35
+ sources: extractGroundingSources(
36
+ asRecord(candidate.groundingMetadata).groundingChunks
37
+ )
50
38
  };
51
39
  },
52
40
  async research(input, config, context, options) {
@@ -91,7 +79,7 @@ var geminiImplementation = {
91
79
  status: "completed",
92
80
  output: {
93
81
  provider: "gemini",
94
- text: text || "Gemini research completed without textual output."
82
+ text
95
83
  }
96
84
  };
97
85
  }
@@ -165,33 +153,18 @@ function readInteractionSteps(interaction) {
165
153
  return typeof interaction === "object" && interaction !== null ? interaction.steps : void 0;
166
154
  }
167
155
  function extractGroundingSources(chunks) {
156
+ if (!Array.isArray(chunks)) return [];
168
157
  const seen = /* @__PURE__ */ new Set();
169
- const sources = [];
170
- const maxSources = 5;
171
- if (!Array.isArray(chunks)) {
172
- return sources;
173
- }
174
- for (const chunk of chunks) {
175
- const web = typeof chunk === "object" && chunk !== null && "web" in chunk && typeof chunk.web === "object" && chunk.web !== null ? chunk.web : void 0;
176
- if (!web) continue;
177
- const rawUrl = typeof web.uri === "string" ? web.uri : "";
178
- const title = formatGroundingSourceTitle(
179
- typeof web.title === "string" ? web.title : rawUrl,
180
- rawUrl
181
- );
182
- const url = formatGroundingSourceUrl(rawUrl);
183
- const key = [title.toLowerCase(), url.toLowerCase()].join("::");
184
- if (seen.has(key)) continue;
158
+ return chunks.flatMap((chunk) => {
159
+ const web = asRecord(asRecord(chunk).web);
160
+ const title = typeof web.title === "string" ? web.title : void 0;
161
+ const uri = typeof web.uri === "string" ? web.uri : void 0;
162
+ const url = uri && !isGoogleGroundingRedirect(uri) ? uri : void 0;
163
+ const key = `${title?.toLowerCase() ?? ""}::${url?.toLowerCase() ?? ""}`;
164
+ if (seen.has(key)) return [];
185
165
  seen.add(key);
186
- sources.push({
187
- title,
188
- url
189
- });
190
- if (sources.length >= maxSources) {
191
- break;
192
- }
193
- }
194
- return sources;
166
+ return [{ title, url }];
167
+ });
195
168
  }
196
169
  function formatInteractionSteps(steps) {
197
170
  const lines = [];
@@ -213,29 +186,6 @@ function formatInteractionSteps(steps) {
213
186
  }
214
187
  return lines.join("\n\n").trim();
215
188
  }
216
- function formatGroundingSourceTitle(title, url) {
217
- const trimmedTitle = title?.trim();
218
- if (trimmedTitle) {
219
- return trimmedTitle;
220
- }
221
- if (url) {
222
- try {
223
- return new URL(url).hostname;
224
- } catch {
225
- return url;
226
- }
227
- }
228
- return "Untitled";
229
- }
230
- function formatGroundingSourceUrl(url) {
231
- if (!url) {
232
- return "";
233
- }
234
- if (isGoogleGroundingRedirect(url)) {
235
- return "";
236
- }
237
- return url;
238
- }
239
189
  function isGoogleGroundingRedirect(url) {
240
190
  try {
241
191
  const parsed = new URL(url);
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-FSTCLBSF.js";
7
7
  import {
8
8
  orderedContents
9
- } from "./chunk-YZXM4U2C.js";
9
+ } from "./chunk-TYJ6RHKB.js";
10
10
  import {
11
11
  WebfoxError,
12
12
  asWebfoxError
@@ -56,15 +56,12 @@ var firecrawlImplementation = {
56
56
  );
57
57
  const document = asRecord(response.data) ?? {};
58
58
  const content = getDocumentContent(document);
59
- return content ? {
59
+ return {
60
60
  url,
61
- content,
61
+ ...content ? { content } : {},
62
62
  ...asRecord(document.metadata) ? {
63
63
  metadata: document.metadata
64
64
  } : {}
65
- } : {
66
- url,
67
- error: "No content returned for this URL."
68
65
  };
69
66
  } catch (error) {
70
67
  return {
@@ -92,14 +89,10 @@ var firecrawlImplementation = {
92
89
  context.signal
93
90
  );
94
91
  const document = getFirecrawlDocument(response);
95
- const answer = readString(document.answer);
96
- if (!answer?.trim()) {
97
- throw new Error("No answer returned for this URL.");
98
- }
99
92
  return {
100
93
  provider: "firecrawl",
101
- text: answer.trim(),
102
- itemCount: 1,
94
+ text: readString(document.answer) ?? "",
95
+ sources: [{ url }],
103
96
  metadata: {
104
97
  url,
105
98
  ...asRecord(document.metadata) ? { metadata: document.metadata } : {}
@@ -209,7 +202,7 @@ function toSearchResult(source, value) {
209
202
  }
210
203
  const metadata = asRecord(entry.metadata);
211
204
  const url = readString(entry.url) ?? readString(metadata?.sourceURL) ?? readString(entry.imageUrl) ?? "";
212
- const title = readString(entry.title) ?? readString(metadata?.title) ?? url;
205
+ const title = readString(entry.title) ?? readString(metadata?.title);
213
206
  const snippet = trimSnippet(
214
207
  readString(entry.description) ?? readString(entry.snippet) ?? readString(entry.markdown) ?? readString(metadata?.description) ?? ""
215
208
  );
@@ -222,7 +215,7 @@ function toSearchResult(source, value) {
222
215
  ...metadata ?? {}
223
216
  };
224
217
  return {
225
- title: title || "Untitled",
218
+ title,
226
219
  url,
227
220
  snippet,
228
221
  metadata: Object.keys(resultMetadata).length > 1 ? resultMetadata : void 0
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-ZVTVERW5.js";
5
5
  import {
6
6
  WEBFOX_ERROR_CODES
7
- } from "./chunk-GPACLYNT.js";
7
+ } from "./chunk-NENL6L6N.js";
8
8
  import {
9
9
  WebfoxError
10
10
  } from "./chunk-U4BLULLV.js";
@@ -145,7 +145,7 @@ var adapter = {
145
145
  results: output.results.map((entry) => {
146
146
  const result = object(entry, "Search results must be objects.");
147
147
  return {
148
- title: string(result.title, "title"),
148
+ ...result.title === void 0 ? {} : { title: string(result.title, "title") },
149
149
  url: string(result.url, "url"),
150
150
  snippet: string(result.snippet, "snippet"),
151
151
  ...typeof result.score === "number" ? { score: result.score } : {},
@@ -169,8 +169,6 @@ var adapter = {
169
169
  "Every custom contents answer requires inputIndex (the zero-based requested URL index)."
170
170
  );
171
171
  const error = answer.error === void 0 ? void 0 : parseError(answer.error);
172
- if (answer.content === void 0 && answer.summary === void 0 && error === void 0)
173
- invalid("Contents answers require content, summary, or error.");
174
172
  return {
175
173
  inputIndex: answer.inputIndex,
176
174
  url: string(answer.url, "url"),
@@ -195,10 +193,20 @@ function textOutput(output) {
195
193
  return {
196
194
  provider: "custom",
197
195
  text: string(output.text, "text"),
198
- ...typeof output.itemCount === "number" && Number.isSafeInteger(output.itemCount) && output.itemCount >= 0 ? { itemCount: output.itemCount } : {},
196
+ ...output.sources === void 0 ? {} : { sources: parseSources(output.sources) },
199
197
  ...output.metadata ? { metadata: object(output.metadata, "metadata must be an object") } : {}
200
198
  };
201
199
  }
200
+ function parseSources(value) {
201
+ if (!Array.isArray(value)) invalid("Custom output sources must be an array.");
202
+ return value.map((entry) => {
203
+ const source = object(entry, "Custom output sources must be objects.");
204
+ return {
205
+ ...source.url === void 0 ? {} : { url: string(source.url, "sources[].url") },
206
+ ...source.title === void 0 ? {} : { title: string(source.title, "sources[].title") }
207
+ };
208
+ });
209
+ }
202
210
  function parseError(value) {
203
211
  const error = object(
204
212
  value,
@@ -2,11 +2,11 @@ import {
2
2
  SERPBASE_MODES,
3
3
  serpbaseProvider,
4
4
  validateOptions
5
- } from "./chunk-LTJOWNI2.js";
5
+ } from "./chunk-PVNWACMT.js";
6
6
  import {
7
7
  trimSnippet
8
8
  } from "./chunk-FSTCLBSF.js";
9
- import "./chunk-GPACLYNT.js";
9
+ import "./chunk-NENL6L6N.js";
10
10
  import {
11
11
  WebfoxError,
12
12
  httpError
@@ -100,7 +100,7 @@ function toSearchResult(entry, searchContext, mode) {
100
100
  if (!url) return void 0;
101
101
  const { title, link: _link, url: _url, snippet, ...metadata } = record;
102
102
  return {
103
- title: text(title) ?? text(record.name) ?? url,
103
+ title: text(title) ?? text(record.name),
104
104
  url,
105
105
  snippet: resultSnippet(record, mode),
106
106
  metadata: { ...metadata, mode, searchContext }
@@ -161,7 +161,7 @@ async function web(query, maxResults, config, context, key, options) {
161
161
  const x = obj(e);
162
162
  const u = str(x.url) ?? "";
163
163
  return {
164
- title: str(x.title) || u || "Untitled",
164
+ title: str(x.title),
165
165
  url: u,
166
166
  snippet: trimSnippet(
167
167
  str(x.description) ?? arr(x.extra_snippets).join(" ")
@@ -211,7 +211,7 @@ async function llmContext(query, maxResults, config, context, key, options) {
211
211
  const source = obj(x.source);
212
212
  const u = str(x.url) ?? str(source.url) ?? "";
213
213
  return {
214
- title: str(x.title) ?? str(x.name) ?? str(source.title) ?? (u || "Untitled"),
214
+ title: str(x.title) ?? str(x.name) ?? str(source.title),
215
215
  url: u,
216
216
  snippet: trimSnippet(snippets.join("\n\n"), 1200),
217
217
  metadata: x
@@ -258,7 +258,7 @@ async function news(query, maxResults, config, context, key, options) {
258
258
  const u = str(x.url) ?? "";
259
259
  const source = str(x.source) ?? str(x.source_name);
260
260
  return {
261
- title: str(x.title) || u || "Untitled",
261
+ title: str(x.title),
262
262
  url: u,
263
263
  snippet: trimSnippet(
264
264
  [
@@ -300,7 +300,7 @@ async function videos(query, maxResults, config, context, key, options) {
300
300
  const u = str(x.url) ?? "";
301
301
  const video = obj(x.video);
302
302
  return {
303
- title: str(x.title) || u || "Untitled",
303
+ title: str(x.title),
304
304
  url: u,
305
305
  snippet: trimSnippet(
306
306
  [
@@ -343,7 +343,7 @@ async function images(query, maxResults, config, context, key, options) {
343
343
  const page = str(x.url) ?? str(x.source) ?? str(props.url) ?? "";
344
344
  const image = str(props.url);
345
345
  return {
346
- title: str(x.title) || page || "Untitled",
346
+ title: str(x.title),
347
347
  url: page || image || "",
348
348
  snippet: trimSnippet(
349
349
  [str(x.description), str(x.publisher), image].filter(Boolean).join(" \u2014 ")
@@ -400,7 +400,7 @@ async function places(query, maxResults, config, context, key, options) {
400
400
  const description = id ? descriptionsById.get(id) : void 0;
401
401
  const u = str(x.url) ?? str(x.provider_url) ?? "";
402
402
  return {
403
- title: str(x.title) || u || "Untitled",
403
+ title: str(x.title),
404
404
  url: u,
405
405
  snippet: trimSnippet(
406
406
  [
@@ -537,19 +537,11 @@ async function completion(input, config, context, request) {
537
537
  if (!r.ok) throw await httpError2(r);
538
538
  const text = await r.text();
539
539
  const parsed = body.stream === false ? parseAnswerJson(text) : parseAnswerStream(text);
540
- const lines = [parsed.answer.trim() || text.trim()];
541
- if (parsed.citations.length) {
542
- lines.push("", "Sources:");
543
- parsed.citations.forEach((c, i) => {
544
- lines.push(`${i + 1}. ${c.title ?? c.url ?? "Source"}`);
545
- if (c.url) lines.push(` ${c.url}`);
546
- });
547
- }
548
540
  const metadata = buildAnswerMetadata(parsed);
549
541
  return {
550
542
  provider: "brave",
551
- text: lines.join("\n").trimEnd(),
552
- itemCount: parsed.citations.length,
543
+ text: parsed.answer,
544
+ sources: parsed.citations,
553
545
  ...metadata ? { metadata } : {}
554
546
  };
555
547
  }
@@ -0,0 +1,71 @@
1
+ // src/render.ts
2
+ function emptyResultText(capability) {
3
+ switch (capability) {
4
+ case "search":
5
+ return "No results found.";
6
+ case "contents":
7
+ return "No content returned.";
8
+ case "answer":
9
+ return "No answer returned.";
10
+ case "research":
11
+ return "No report returned.";
12
+ }
13
+ }
14
+ function resultLabel(result) {
15
+ return result.title ?? ("url" in result ? result.url : "");
16
+ }
17
+ function renderTextDocument(document, { includeErrors = true } = {}) {
18
+ return document.results.map((entry, index) => {
19
+ const heading = document.results.length > 1 ? `## ${index + 1}. ${entry.input}` : "";
20
+ if (!entry.ok)
21
+ return includeErrors ? [heading, `Error: ${entry.error.message}`].filter(Boolean).join("\n\n") : void 0;
22
+ let body;
23
+ if ("results" in entry.value)
24
+ body = renderSearch(entry.value.results, document.capability);
25
+ else if ("text" in entry.value)
26
+ body = renderText(entry.value, document.capability);
27
+ else body = renderContents(entry.value, document.capability);
28
+ return [heading, body].filter(Boolean).join("\n\n");
29
+ }).filter((part) => part !== void 0).join("\n\n");
30
+ }
31
+ function renderSearch(results, capability) {
32
+ if (!results.length) return emptyResultText(capability);
33
+ return results.map(
34
+ (result, i) => [
35
+ `${i + 1}. ${resultLabel(result)}`,
36
+ result.title === void 0 ? void 0 : ` ${result.url}`,
37
+ result.snippet.trim() ? ` ${result.snippet.replaceAll("\n", "\n ")}` : void 0
38
+ ].filter((line) => line !== void 0).join("\n")
39
+ ).join("\n\n");
40
+ }
41
+ function renderText(answer, capability) {
42
+ const lines = [
43
+ answer.text.trim() ? answer.text : emptyResultText(capability)
44
+ ];
45
+ if (answer.sources?.length) {
46
+ lines.push("", "Sources:");
47
+ for (const [index, source] of answer.sources.entries()) {
48
+ lines.push(`${index + 1}. ${resultLabel(source)}`);
49
+ if ("url" in source && source.title && source.title !== source.url)
50
+ lines.push(` ${source.url}`);
51
+ }
52
+ }
53
+ return lines.join("\n");
54
+ }
55
+ function renderContents(answer, capability) {
56
+ const content = answer.content?.trim() ? answer.content : void 0;
57
+ const summary = typeof answer.summary === "string" ? answer.summary.trim() ? answer.summary : void 0 : answer.summary == null ? void 0 : JSON.stringify(answer.summary, null, 2);
58
+ return [
59
+ `## ${answer.url}`,
60
+ content,
61
+ summary === void 0 ? void 0 : `### Summary
62
+
63
+ ${summary}`,
64
+ content === void 0 && summary === void 0 ? emptyResultText(capability) : void 0
65
+ ].filter((part) => part !== void 0).join("\n\n");
66
+ }
67
+
68
+ export {
69
+ emptyResultText,
70
+ renderTextDocument
71
+ };
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  optionSchema,
3
3
  providers
4
- } from "./chunk-LTJOWNI2.js";
4
+ } from "./chunk-PVNWACMT.js";
5
5
  import {
6
6
  CAPABILITIES
7
- } from "./chunk-GPACLYNT.js";
7
+ } from "./chunk-NENL6L6N.js";
8
8
 
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "webfox",
12
- version: "4.9.0",
12
+ version: "4.11.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: [
@@ -56,10 +56,7 @@ async function executeAsyncResearch({
56
56
  continue;
57
57
  }
58
58
  if (result.status === "completed")
59
- return result.output ?? {
60
- provider: providerId,
61
- text: `${providerLabel} research completed without textual output.`
62
- };
59
+ return result.output ?? { provider: providerId, text: "" };
63
60
  if (result.status === "failed" || result.status === "cancelled")
64
61
  throw new WebfoxError(
65
62
  result.status === "cancelled" ? "CANCELLED" : "PROVIDER_FAILURE",
@@ -12,6 +12,7 @@ var PROVIDER_IDS = [
12
12
  "exa",
13
13
  "firecrawl",
14
14
  "gemini",
15
+ "keenable",
15
16
  "linkup",
16
17
  "ollama",
17
18
  "openai",
@@ -19,6 +20,7 @@ var PROVIDER_IDS = [
19
20
  "perplexity",
20
21
  "serpbase",
21
22
  "serper",
23
+ "sofya",
22
24
  "tavily",
23
25
  "valyu",
24
26
  "youcom"