@xl0/pi-lovely-web 0.1.3 → 0.2.1

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,7 +1,7 @@
1
1
  import { formatDimensionNote, type ResizedImage, resizeImage } from "@earendil-works/pi-coding-agent"
2
2
  import { getImageDimensions } from "@earendil-works/pi-tui"
3
- import { DEFAULT_TIMEOUT_MS } from "./config.js"
4
- import type { ToolResult } from "./tool-impl.js"
3
+ import { DEFAULT_TIMEOUT_MS } from "./constants.js"
4
+ import type { ToolResult } from "./types.js"
5
5
 
6
6
  export const DEFAULT_MAX_IMAGE_BYTES = 5_000_000
7
7
  export const MAX_IMAGE_BYTES = 20_000_000
@@ -63,6 +63,25 @@ async function fetchImageContent(
63
63
  }
64
64
  }
65
65
 
66
+ function textOnlyResult(text: string, details: Record<string, unknown>): ToolResult {
67
+ return { content: [{ type: "text" as const, text }], details }
68
+ }
69
+
70
+ function imageResult(text: string, image: { data: string; mimeType: string }, details: Record<string, unknown>): ToolResult {
71
+ return {
72
+ content: [
73
+ { type: "text" as const, text },
74
+ { type: "image" as const, data: image.data, mimeType: image.mimeType }
75
+ ],
76
+ details
77
+ }
78
+ }
79
+
80
+ function emitResult(result: ToolResult, onUpdate?: (result: ToolResult) => void): ToolResult {
81
+ onUpdate?.(result)
82
+ return result
83
+ }
84
+
66
85
  export async function imageImpl(
67
86
  params: {
68
87
  url: string
@@ -81,87 +100,58 @@ export async function imageImpl(
81
100
  if (signal?.aborted) throw new Error("Image fetch cancelled")
82
101
 
83
102
  const originalDimensions = getImageDimensions(image.data, image.mimeType) ?? undefined
103
+ const downloadDetails = {
104
+ url: params.url,
105
+ mimeType: image.mimeType,
106
+ bytes: image.bytes,
107
+ contentLength: image.contentLength
108
+ }
84
109
 
85
110
  if (params.resize === false) {
86
111
  // Re-encode to sanitize, but don't downscale.
87
- const validated = (await resizeImage(
88
- { type: "image", data: image.data, mimeType: image.mimeType },
89
- { maxWidth: Infinity, maxHeight: Infinity }
90
- )) as ResizedImage | null
112
+ const validated = (await resizeImage(Buffer.from(image.data, "base64"), image.mimeType, {
113
+ maxWidth: Infinity,
114
+ maxHeight: Infinity
115
+ })) as ResizedImage | null
91
116
  if (!validated) {
92
117
  const note = `Fetched image [${image.mimeType}]\n[Image omitted: could not be decoded]`
93
- const result: ToolResult = {
94
- content: [{ type: "text" as const, text: note }],
95
- details: {
96
- url: params.url,
97
- mimeType: image.mimeType,
98
- bytes: image.bytes,
99
- contentLength: image.contentLength
100
- }
101
- }
102
- onUpdate?.(result)
103
- return result
118
+ return emitResult(textOnlyResult(note, downloadDetails), onUpdate)
104
119
  }
105
120
  const dimensions = { widthPx: validated.width, heightPx: validated.height }
106
121
  const note = `Fetched image [${validated.mimeType}]`
107
- const result: ToolResult = {
108
- content: [
109
- { type: "text" as const, text: note },
110
- { type: "image" as const, data: validated.data, mimeType: validated.mimeType }
111
- ],
112
- details: {
113
- url: params.url,
122
+ return emitResult(
123
+ imageResult(note, validated, {
124
+ ...downloadDetails,
114
125
  mimeType: validated.mimeType,
115
- bytes: image.bytes,
116
- contentLength: image.contentLength,
117
126
  dimensions,
118
127
  originalDimensions,
119
128
  wasResized: validated.wasResized
120
- }
121
- }
122
- onUpdate?.(result)
123
- return result
129
+ }),
130
+ onUpdate
131
+ )
124
132
  }
125
133
 
126
- const resized = (await resizeImage(
127
- { type: "image", data: image.data, mimeType: image.mimeType },
128
- { maxWidth: params.maxSize ?? 2000, maxHeight: params.maxSize ?? 2000 }
129
- )) as ResizedImage | null
134
+ const resized = (await resizeImage(Buffer.from(image.data, "base64"), image.mimeType, {
135
+ maxWidth: params.maxSize ?? 2000,
136
+ maxHeight: params.maxSize ?? 2000
137
+ })) as ResizedImage | null
130
138
 
131
139
  if (!resized) {
132
140
  const note = `Fetched image [${image.mimeType}]\n[Image omitted: could not be decoded or resized below the inline image size limit.]`
133
- const result: ToolResult = {
134
- content: [{ type: "text" as const, text: note }],
135
- details: {
136
- url: params.url,
137
- mimeType: image.mimeType,
138
- bytes: image.bytes,
139
- contentLength: image.contentLength,
140
- dimensions: originalDimensions
141
- }
142
- }
143
- onUpdate?.(result)
144
- return result
141
+ return emitResult(textOnlyResult(note, { ...downloadDetails, dimensions: originalDimensions }), onUpdate)
145
142
  }
146
143
 
147
144
  const dimensionNote = formatDimensionNote(resized)
148
145
  const note = `Fetched image [${resized.mimeType}]${dimensionNote ? `\n${dimensionNote}` : ""}`
149
146
  const dimensions = { widthPx: resized.width, heightPx: resized.height }
150
- const result: ToolResult = {
151
- content: [
152
- { type: "text" as const, text: note },
153
- { type: "image" as const, data: resized.data, mimeType: resized.mimeType }
154
- ],
155
- details: {
156
- url: params.url,
147
+ return emitResult(
148
+ imageResult(note, resized, {
149
+ ...downloadDetails,
157
150
  mimeType: resized.mimeType,
158
- bytes: image.bytes,
159
- contentLength: image.contentLength,
160
151
  dimensions,
161
152
  originalDimensions,
162
153
  wasResized: resized.wasResized
163
- }
164
- }
165
- onUpdate?.(result)
166
- return result
154
+ }),
155
+ onUpdate
156
+ )
167
157
  }
@@ -1,12 +1,21 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"
2
2
  import { registerLovelyWebCommand } from "./command.js"
3
- import { applyToolConfig, loadConfig } from "./config.js"
4
- import { registerLovelyWebTools } from "./tools.js"
3
+ import { applyToolConfig, loadScopedConfig } from "./config.js"
4
+ import { asErrorMessage } from "./format.js"
5
+ import { registerLovelyWebSearchTool, registerLovelyWebStaticTools } from "./tools.js"
5
6
 
6
7
  export default function (pi: ExtensionAPI) {
8
+ registerLovelyWebStaticTools(pi)
7
9
  pi.on("session_start", async (_event, ctx) => {
8
- applyToolConfig(pi, loadConfig(ctx.cwd))
10
+ try {
11
+ const loaded = loadScopedConfig(ctx.cwd)
12
+ if (loaded.warnings.length > 0) ctx.ui.notify(loaded.warnings.map(w => `${w.path}: ${w.message}`).join("\n"), "warning")
13
+ registerLovelyWebSearchTool(pi, loaded.value)
14
+ registerLovelyWebStaticTools(pi, loaded.value)
15
+ applyToolConfig(pi, loaded.value)
16
+ } catch (error) {
17
+ ctx.ui.notify(`Lovely Web config error: ${asErrorMessage(error)}`, "error")
18
+ }
9
19
  })
10
- registerLovelyWebTools(pi)
11
20
  registerLovelyWebCommand(pi)
12
21
  }
@@ -1,8 +1,10 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai"
2
+ import { Type } from "typebox"
3
+ import { DEFAULT_TIMEOUT_MS } from "../constants.js"
1
4
  import { requestJson } from "./http.js"
2
5
  import type { Provider, SearchResult } from "./types.js"
3
6
 
4
7
  const BASE_URL = "https://api.search.brave.com/res/v1"
5
- const DEFAULT_TIMEOUT_MS = 30_000
6
8
 
7
9
  interface BraveWebResult {
8
10
  title: string
@@ -28,6 +30,8 @@ interface BraveImageResult {
28
30
  url: string
29
31
  title?: string
30
32
  description?: string
33
+ properties?: { url?: string }
34
+ thumbnail?: { src?: string }
31
35
  }
32
36
 
33
37
  interface BraveImageResponse {
@@ -53,24 +57,35 @@ function fetchJson(url: string, apiKey: string, timeout: number, signal?: AbortS
53
57
  )
54
58
  }
55
59
 
56
- function buildSearchUrl(source: string | undefined, query: string, count: number): string {
57
- const encodedQuery = encodeURIComponent(query)
58
- if (source === "news") {
59
- return `${BASE_URL}/news/search?q=${encodedQuery}&count=${count}`
60
- }
61
- if (source === "images") {
62
- return `${BASE_URL}/images/search?q=${encodedQuery}&count=${count}`
63
- }
64
- return `${BASE_URL}/web/search?q=${encodedQuery}&count=${count}`
60
+ function buildSearchUrl(
61
+ source: string | undefined,
62
+ query: string,
63
+ count: number,
64
+ opts: { country?: string; searchLang?: string; freshness?: string }
65
+ ): string {
66
+ const endpoint = source === "news" ? "news" : source === "images" ? "images" : "web"
67
+ const url = new URL(`${BASE_URL}/${endpoint}/search`)
68
+ url.searchParams.set("q", query)
69
+ url.searchParams.set("count", String(count))
70
+ if (opts.country) url.searchParams.set("country", opts.country.toUpperCase())
71
+ if (opts.searchLang) url.searchParams.set("search_lang", opts.searchLang)
72
+ if (opts.freshness && source !== "images") url.searchParams.set("freshness", opts.freshness)
73
+ return url.toString()
65
74
  }
66
75
 
67
76
  export const braveProvider: Provider = {
68
77
  id: "brave",
69
78
  label: "Brave Search",
70
79
  envApiKey: "BRAVE_API_KEY",
80
+ searchParameters: {
81
+ source: Type.Optional(StringEnum(["web", "news", "images"])),
82
+ country: Type.Optional(Type.String({ description: "Two-letter country code, e.g. US, DE, CO, or ALL." })),
83
+ searchLang: Type.Optional(Type.String({ description: "Search language code, e.g. en, es, de." })),
84
+ freshness: Type.Optional(Type.String({ description: "Freshness filter for web/news: pd, pw, pm, py, or YYYY-MM-DDtoYYYY-MM-DD." }))
85
+ },
71
86
 
72
87
  async search(apiKey, query, opts, signal) {
73
- const url = buildSearchUrl(opts.source, query, opts.limit)
88
+ const url = buildSearchUrl(opts.source, query, opts.limit, opts)
74
89
  const raw = await fetchJson(url, apiKey, opts.timeout ?? DEFAULT_TIMEOUT_MS, signal)
75
90
 
76
91
  let items: SearchResult[] = []
@@ -85,9 +100,9 @@ export const braveProvider: Provider = {
85
100
  } else if (opts.source === "images") {
86
101
  const data = raw as BraveImageResponse
87
102
  items = (data.results ?? []).map(item => ({
88
- title: item.title || item.url,
89
- url: item.url,
90
- description: item.description || ""
103
+ title: item.title || item.properties?.url || item.url,
104
+ url: item.properties?.url || item.thumbnail?.src || item.url,
105
+ description: item.description || item.url || ""
91
106
  }))
92
107
  } else {
93
108
  const data = raw as BraveWebResponse
@@ -1,8 +1,10 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai"
2
+ import { Type } from "typebox"
3
+ import { DEFAULT_TIMEOUT_MS } from "../constants.js"
1
4
  import { requestJson } from "./http.js"
2
5
  import type { Provider, SearchResult } from "./types.js"
3
6
 
4
7
  const BASE_URL = "https://api.exa.ai"
5
- const DEFAULT_TIMEOUT_MS = 30_000
6
8
 
7
9
  interface SearchBody {
8
10
  query: string
@@ -10,11 +12,13 @@ interface SearchBody {
10
12
  type: string
11
13
  contents: { summary: boolean }
12
14
  category?: string
15
+ userLocation?: string
13
16
  }
14
17
 
15
18
  interface ContentsBody {
16
- ids: string[]
19
+ urls: string[]
17
20
  text: boolean
21
+ maxAgeHours?: number
18
22
  }
19
23
 
20
24
  interface ExaSearchResult {
@@ -30,11 +34,6 @@ interface ExaContentsResult {
30
34
  text?: string
31
35
  }
32
36
 
33
- function sourceToCategory(source?: string): string | undefined {
34
- if (source === "news") return "news"
35
- return undefined
36
- }
37
-
38
37
  function stripSummaryLabel(text: string): string {
39
38
  return text.replace(/^Summary:\s*/i, "")
40
39
  }
@@ -60,6 +59,15 @@ export const exaProvider: Provider = {
60
59
  id: "exa",
61
60
  label: "Exa",
62
61
  envApiKey: "EXA_API_KEY",
62
+ searchParameters: {
63
+ category: Type.Optional(StringEnum(["company", "people", "research paper", "news", "personal site", "financial report"])),
64
+ country: Type.Optional(Type.String({ description: "Two-letter ISO user location for result localization, e.g. US, DE, CO." }))
65
+ },
66
+ fetchParameters: {
67
+ maxAgeHours: Type.Optional(
68
+ Type.Integer({ description: "Maximum cache age in hours; 0 fetches fresh content, -1 uses cache only.", minimum: -1, maximum: 720 })
69
+ )
70
+ },
63
71
 
64
72
  async search(apiKey, query, opts, signal) {
65
73
  const body: SearchBody = {
@@ -68,8 +76,9 @@ export const exaProvider: Provider = {
68
76
  type: "auto",
69
77
  contents: { summary: true }
70
78
  }
71
- const category = sourceToCategory(opts.source)
72
- if (category) body.category = category
79
+ if (opts.source) throw new Error("Exa search uses category, not source. Omit category for general web search, or set category: news.")
80
+ if (opts.category) body.category = opts.category
81
+ if (opts.country) body.userLocation = opts.country
73
82
 
74
83
  const raw = await fetchJson(`${BASE_URL}/search`, body, apiKey, opts.timeout ?? DEFAULT_TIMEOUT_MS, signal)
75
84
  const data = (raw as { results?: ExaSearchResult[] }).results ?? []
@@ -88,9 +97,10 @@ export const exaProvider: Provider = {
88
97
 
89
98
  async fetch(apiKey, url, opts, signal) {
90
99
  const body: ContentsBody = {
91
- ids: [url],
100
+ urls: [url],
92
101
  text: true
93
102
  }
103
+ if (opts.maxAgeHours !== undefined) body.maxAgeHours = opts.maxAgeHours
94
104
 
95
105
  const raw = await fetchJson(`${BASE_URL}/contents`, body, apiKey, opts.timeout ?? DEFAULT_TIMEOUT_MS, signal)
96
106
  const results = (raw as { results?: ExaContentsResult[] }).results
@@ -1,13 +1,19 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai"
2
+ import { Type } from "typebox"
3
+ import { DEFAULT_TIMEOUT_MS } from "../constants.js"
1
4
  import { requestJson } from "./http.js"
2
5
  import type { Provider, SearchResult } from "./types.js"
3
6
 
4
- const BASE_URL = "https://api.firecrawl.dev/v1"
5
- const DEFAULT_TIMEOUT_MS = 30_000
7
+ const BASE_URL = "https://api.firecrawl.dev/v2"
6
8
 
7
9
  interface SearchBody {
8
10
  query: string
9
11
  limit: number
10
12
  sources?: string[]
13
+ categories?: string[]
14
+ location?: string
15
+ country?: string
16
+ tbs?: string
11
17
  }
12
18
 
13
19
  interface ScrapeBody {
@@ -52,18 +58,43 @@ export const firecrawlProvider: Provider = {
52
58
  id: "firecrawl",
53
59
  label: "Firecrawl",
54
60
  envApiKey: "FIRECRAWL_API_KEY",
61
+ searchParameters: {
62
+ source: Type.Optional(StringEnum(["web", "news", "images"])),
63
+ category: Type.Optional(StringEnum(["github", "research", "pdf"])),
64
+ location: Type.Optional(Type.String({ description: "Geo-target results, e.g. Germany or San Francisco,California,United States." })),
65
+ country: Type.Optional(Type.String({ description: "ISO country code for geo-targeting, e.g. US, DE, CO." })),
66
+ tbs: Type.Optional(Type.String({ description: "Google-style time filter, e.g. qdr:d, qdr:w, qdr:m, qdr:y." }))
67
+ },
68
+ fetchParameters: {
69
+ waitFor: Type.Optional(Type.Integer({ description: "Extra delay in milliseconds before capturing page content.", minimum: 0 }))
70
+ },
55
71
 
56
72
  async search(apiKey, query, opts, signal) {
57
73
  const body: SearchBody = { query, limit: opts.limit }
58
74
  if (opts.source) body.sources = [opts.source]
75
+ if (opts.category) body.categories = [opts.category]
76
+ if (opts.location) body.location = opts.location
77
+ if (opts.country) body.country = opts.country
78
+ if (opts.tbs) body.tbs = opts.tbs
59
79
 
60
80
  const raw = await fetchJson(`${BASE_URL}/search`, body, apiKey, opts.timeout ?? DEFAULT_TIMEOUT_MS, signal)
61
81
  throwIfError(raw)
62
- const data = raw.data as Array<{ title: string; url: string; description: string }>
82
+ const data = raw.data as {
83
+ web?: Array<{ title: string; url: string; description?: string; markdown?: string }>
84
+ news?: Array<{ title: string; url: string; snippet?: string; markdown?: string }>
85
+ images?: Array<{ title?: string; url: string; imageUrl?: string }>
86
+ }
87
+ const items = opts.source === "news" ? data.news || [] : opts.source === "images" ? data.images || [] : data.web || []
63
88
 
64
- const results: SearchResult[] = data.map(item => {
65
- const r: SearchResult = { title: item.title, url: item.url }
66
- if (item.description) r.description = item.description
89
+ const results: SearchResult[] = items.map(item => {
90
+ const isImage = opts.source === "images"
91
+ const r: SearchResult = {
92
+ title: item.title || item.url,
93
+ url: isImage && "imageUrl" in item && item.imageUrl ? item.imageUrl : item.url
94
+ }
95
+ const description = "description" in item ? item.description : "snippet" in item ? item.snippet : undefined
96
+ if (description) r.description = description
97
+ if ("markdown" in item && item.markdown) r.markdown = item.markdown
67
98
  return r
68
99
  })
69
100
 
@@ -1,14 +1,20 @@
1
+ import { StringEnum } from "@earendil-works/pi-ai"
2
+ import { Type } from "typebox"
3
+ import { DEFAULT_TIMEOUT_MS } from "../constants.js"
1
4
  import { requestJson } from "./http.js"
2
5
  import type { Provider, SearchResult } from "./types.js"
3
6
 
4
7
  const BASE_URL = "https://api.tavily.com"
5
- const DEFAULT_TIMEOUT_MS = 30_000
6
8
 
7
9
  interface SearchBody {
8
10
  query: string
9
11
  max_results: number
10
12
  search_depth: string
11
13
  topic?: string
14
+ time_range?: string
15
+ country?: string
16
+ include_images?: boolean
17
+ include_image_descriptions?: boolean
12
18
  }
13
19
 
14
20
  interface ExtractBody {
@@ -25,11 +31,16 @@ interface TavilySearchResult {
25
31
  score: number
26
32
  }
27
33
 
34
+ interface TavilyImageResult {
35
+ url: string
36
+ description?: string
37
+ }
38
+
28
39
  interface TavilySearchResponse {
29
40
  query: string
30
41
  answer?: string
31
42
  results?: TavilySearchResult[]
32
- images?: unknown[]
43
+ images?: TavilyImageResult[]
33
44
  response_time: number
34
45
  }
35
46
 
@@ -46,11 +57,6 @@ interface TavilyExtractResponse {
46
57
  response_time: number
47
58
  }
48
59
 
49
- function sourceToTopic(source?: string): string | undefined {
50
- if (source === "news") return "news"
51
- return undefined
52
- }
53
-
54
60
  function postJson(url: string, body: unknown, apiKey: string, timeout: number, signal?: AbortSignal): Promise<unknown> {
55
61
  return requestJson(
56
62
  "Tavily",
@@ -72,6 +78,17 @@ export const tavilyProvider: Provider = {
72
78
  id: "tavily",
73
79
  label: "Tavily",
74
80
  envApiKey: "TAVILY_API_KEY",
81
+ searchParameters: {
82
+ topic: Type.Optional(StringEnum(["general", "news", "finance"])),
83
+ includeImages: Type.Optional(Type.Boolean({ description: "Return query-related image URLs instead of page results." })),
84
+ country: Type.Optional(Type.String({ description: "Country name to boost general-topic results, e.g. colombia." })),
85
+ timeRange: Type.Optional(StringEnum(["day", "week", "month", "year", "d", "w", "m", "y"]))
86
+ },
87
+ fetchParameters: {
88
+ extractDepth: Type.Optional(
89
+ StringEnum(["basic", "advanced"], { description: "Advanced extraction retrieves more data such as tables; costs more." })
90
+ )
91
+ },
75
92
 
76
93
  async search(apiKey, query, opts, signal) {
77
94
  const body: SearchBody = {
@@ -79,11 +96,28 @@ export const tavilyProvider: Provider = {
79
96
  max_results: opts.limit,
80
97
  search_depth: "basic"
81
98
  }
82
- const topic = sourceToTopic(opts.source)
83
- if (topic) body.topic = topic
99
+ if (opts.source) throw new Error("Tavily search uses topic/includeImages, not source.")
100
+ const topic = opts.topic
101
+ if (topic && topic !== "general") body.topic = topic
102
+ if (opts.timeRange) body.time_range = opts.timeRange
103
+ if (opts.country && topic !== "news" && topic !== "finance") body.country = opts.country.toLowerCase()
104
+ if (opts.includeImages) {
105
+ body.include_images = true
106
+ body.include_image_descriptions = true
107
+ }
84
108
 
85
109
  const raw = await postJson(`${BASE_URL}/search`, body, apiKey, opts.timeout ?? DEFAULT_TIMEOUT_MS, signal)
86
110
  const data = raw as TavilySearchResponse
111
+
112
+ if (opts.includeImages) {
113
+ const results: SearchResult[] = (data.images ?? []).map(item => ({
114
+ title: item.description || item.url,
115
+ url: item.url,
116
+ description: item.description || ""
117
+ }))
118
+ return { results, raw }
119
+ }
120
+
87
121
  const items = data.results ?? []
88
122
 
89
123
  const results: SearchResult[] = items.map(item => {
@@ -101,7 +135,7 @@ export const tavilyProvider: Provider = {
101
135
  async fetch(apiKey, url, opts, signal) {
102
136
  const body: ExtractBody = {
103
137
  urls: [url],
104
- extract_depth: "basic",
138
+ extract_depth: opts.extractDepth ?? "basic",
105
139
  format: "markdown"
106
140
  }
107
141
 
@@ -1,3 +1,5 @@
1
+ import type { TSchema } from "typebox"
2
+
1
3
  export interface SearchResult {
2
4
  title: string
3
5
  url: string
@@ -5,27 +7,39 @@ export interface SearchResult {
5
7
  markdown?: string // populated for first result when fetchResult=true
6
8
  }
7
9
 
10
+ export interface SearchOptions {
11
+ limit: number
12
+ source?: string
13
+ timeout?: number
14
+ category?: string
15
+ location?: string
16
+ country?: string
17
+ tbs?: string
18
+ timeRange?: string
19
+ topic?: string
20
+ includeImages?: boolean
21
+ searchLang?: string
22
+ freshness?: string
23
+ }
24
+
25
+ export interface FetchOptions {
26
+ timeout?: number
27
+ waitFor?: number
28
+ maxAgeHours?: number
29
+ extractDepth?: string
30
+ }
31
+
8
32
  export interface Provider {
9
33
  readonly id: string
10
34
  readonly label: string
11
35
  readonly envApiKey: string
12
- search(
13
- apiKey: string,
14
- query: string,
15
- opts: { limit: number; source?: string; timeout?: number },
16
- signal?: AbortSignal
17
- ): Promise<{ results: SearchResult[]; raw: unknown }>
36
+ readonly searchParameters?: Record<string, TSchema>
37
+ readonly fetchParameters?: Record<string, TSchema>
38
+ search(apiKey: string, query: string, opts: SearchOptions, signal?: AbortSignal): Promise<{ results: SearchResult[]; raw: unknown }>
18
39
  fetch?(
19
40
  apiKey: string,
20
41
  url: string,
21
- opts: { waitFor?: number; timeout?: number },
42
+ opts: FetchOptions,
22
43
  signal?: AbortSignal
23
44
  ): Promise<{ markdown: string; metadata?: unknown; raw: unknown }>
24
45
  }
25
-
26
- export interface WebToolsConfig {
27
- webSearch?: { provider?: string | null }
28
- webFetch?: { provider?: string | null }
29
- webImage?: { enabled?: boolean; resize?: boolean; maxSize?: number }
30
- webApiKeys?: Record<string, string>
31
- }