@pentoshi/clai 0.10.4 → 0.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 (82) hide show
  1. package/README.md +32 -0
  2. package/dist/agent/runner.js +41 -3
  3. package/dist/agent/runner.js.map +1 -1
  4. package/dist/commands/providers.js +28 -0
  5. package/dist/commands/providers.js.map +1 -1
  6. package/dist/commands/search-providers.d.ts +50 -0
  7. package/dist/commands/search-providers.js +134 -0
  8. package/dist/commands/search-providers.js.map +1 -0
  9. package/dist/commands/update.js +1 -1
  10. package/dist/index.js +8 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/llm/provider.js +9 -6
  13. package/dist/llm/provider.js.map +1 -1
  14. package/dist/prompts/index.d.ts +1 -1
  15. package/dist/prompts/index.js +6 -0
  16. package/dist/prompts/index.js.map +1 -1
  17. package/dist/repl.d.ts +1 -0
  18. package/dist/repl.js +139 -113
  19. package/dist/repl.js.map +1 -1
  20. package/dist/safety/classifier.js +40 -0
  21. package/dist/safety/classifier.js.map +1 -1
  22. package/dist/store/config.d.ts +5 -0
  23. package/dist/store/config.js +7 -0
  24. package/dist/store/config.js.map +1 -1
  25. package/dist/store/keys.d.ts +65 -0
  26. package/dist/store/keys.js +164 -28
  27. package/dist/store/keys.js.map +1 -1
  28. package/dist/tools/http.d.ts +12 -1
  29. package/dist/tools/http.js +8 -43
  30. package/dist/tools/http.js.map +1 -1
  31. package/dist/tools/registry.js +52 -0
  32. package/dist/tools/registry.js.map +1 -1
  33. package/dist/tools/shell.d.ts +25 -0
  34. package/dist/tools/shell.js +155 -6
  35. package/dist/tools/shell.js.map +1 -1
  36. package/dist/tools/web/audit.d.ts +154 -0
  37. package/dist/tools/web/audit.js +147 -0
  38. package/dist/tools/web/audit.js.map +1 -0
  39. package/dist/tools/web/budget.d.ts +76 -0
  40. package/dist/tools/web/budget.js +187 -0
  41. package/dist/tools/web/budget.js.map +1 -0
  42. package/dist/tools/web/capture.d.ts +201 -0
  43. package/dist/tools/web/capture.js +380 -0
  44. package/dist/tools/web/capture.js.map +1 -0
  45. package/dist/tools/web/fetch-core.d.ts +66 -0
  46. package/dist/tools/web/fetch-core.js +1123 -0
  47. package/dist/tools/web/fetch-core.js.map +1 -0
  48. package/dist/tools/web/fetch.d.ts +42 -0
  49. package/dist/tools/web/fetch.js +115 -0
  50. package/dist/tools/web/fetch.js.map +1 -0
  51. package/dist/tools/web/providers/brave.d.ts +46 -0
  52. package/dist/tools/web/providers/brave.js +263 -0
  53. package/dist/tools/web/providers/brave.js.map +1 -0
  54. package/dist/tools/web/providers/duckduckgo.d.ts +47 -0
  55. package/dist/tools/web/providers/duckduckgo.js +248 -0
  56. package/dist/tools/web/providers/duckduckgo.js.map +1 -0
  57. package/dist/tools/web/providers/provider.d.ts +99 -0
  58. package/dist/tools/web/providers/provider.js +38 -0
  59. package/dist/tools/web/providers/provider.js.map +1 -0
  60. package/dist/tools/web/providers/tavily.d.ts +52 -0
  61. package/dist/tools/web/providers/tavily.js +285 -0
  62. package/dist/tools/web/providers/tavily.js.map +1 -0
  63. package/dist/tools/web/readable.d.ts +67 -0
  64. package/dist/tools/web/readable.js +248 -0
  65. package/dist/tools/web/readable.js.map +1 -0
  66. package/dist/tools/web/redact.d.ts +120 -0
  67. package/dist/tools/web/redact.js +155 -0
  68. package/dist/tools/web/redact.js.map +1 -0
  69. package/dist/tools/web/search.d.ts +51 -0
  70. package/dist/tools/web/search.js +389 -0
  71. package/dist/tools/web/search.js.map +1 -0
  72. package/dist/tools/web/ssrf-guard.d.ts +85 -0
  73. package/dist/tools/web/ssrf-guard.js +265 -0
  74. package/dist/tools/web/ssrf-guard.js.map +1 -0
  75. package/dist/tools/web/types.d.ts +331 -0
  76. package/dist/tools/web/types.js +71 -0
  77. package/dist/tools/web/types.js.map +1 -0
  78. package/dist/ui/keys.js +3 -2
  79. package/dist/ui/keys.js.map +1 -1
  80. package/dist/ui/spinner.js +87 -14
  81. package/dist/ui/spinner.js.map +1 -1
  82. package/package.json +3 -1
@@ -0,0 +1,248 @@
1
+ /**
2
+ * DuckDuckGo search-provider adapter.
3
+ *
4
+ * Used as the keyless default so `clai` works out of the box without
5
+ * any API key (Requirement 3.5). The adapter targets DuckDuckGo's
6
+ * lite-HTML endpoint at `https://html.duckduckgo.com/html/?q=…`,
7
+ * parses the response with `cheerio`, unwraps the in-page redirect
8
+ * shim (`/l/?uddg=<encoded>`) so callers see the destination URL, and
9
+ * applies the same URL filter `web.search` enforces (Requirement 7.3)
10
+ * before forwarding hits to the registry handler.
11
+ *
12
+ * The adapter does not need redirect-chain capture, fine-grained
13
+ * timing, TLS metadata, or DNS-rebinding protection beyond the
14
+ * per-invocation `AbortSignal`, so it deliberately bypasses the full
15
+ * `fetch-core` pipeline. A small `node:https.request`-based helper
16
+ * keeps the implementation simple while still honoring the 15-second
17
+ * `web.search` invocation timeout (Requirement 1.8) via the supplied
18
+ * `AbortSignal`.
19
+ *
20
+ * Per Requirement 6.7 the adapter issues exactly one outbound request
21
+ * per invocation and never retries on transient failure.
22
+ */
23
+ import { Buffer } from "node:buffer";
24
+ import https from "node:https";
25
+ import * as cheerio from "cheerio";
26
+ import { searchProviders } from "./provider.js";
27
+ /** Endpoint for DuckDuckGo's keyless lite-HTML search. */
28
+ const ENDPOINT = "https://html.duckduckgo.com/html/";
29
+ /** User-Agent sent on the outbound DDG request. */
30
+ const USER_AGENT = "clai-web-search/1.0";
31
+ let httpsRequestFn = https.request;
32
+ /**
33
+ * Test-only seam: swap the HTTPS transport used by the adapter. Tests use
34
+ * this to inject a stubbed `request` implementation that emits scripted
35
+ * responses; production callers never invoke it.
36
+ */
37
+ export function __setDuckduckgoHttpsRequestForTesting(fn) {
38
+ httpsRequestFn = fn ?? https.request;
39
+ }
40
+ /**
41
+ * Cap on the bytes read from DDG's lite-HTML response. The page is
42
+ * typically well under 200 KiB; the cap exists purely as a memory
43
+ * guard so a misbehaving server cannot stream us an unbounded body.
44
+ */
45
+ const MAX_RESPONSE_BYTES = 1_048_576;
46
+ /**
47
+ * Issue a single GET request over HTTPS, honoring the supplied
48
+ * abort signal (which `web.search` arms to a 15-second timer).
49
+ *
50
+ * Reads at most {@link MAX_RESPONSE_BYTES}, decodes as UTF-8 with
51
+ * replacement of invalid sequences, and resolves with `{status, body}`.
52
+ * Network failures and aborts surface as a rejected promise so the
53
+ * registry handler can map them to the appropriate
54
+ * `WebSearchErrorKind`.
55
+ *
56
+ * Redirect-chain capture is intentionally omitted here — DDG's lite
57
+ * endpoint replies 200 directly in normal operation, and any 3xx is
58
+ * treated as an empty result by the caller.
59
+ */
60
+ function httpsGetText(url, signal) {
61
+ return new Promise((resolve, reject) => {
62
+ let req;
63
+ try {
64
+ req = httpsRequestFn(url, {
65
+ method: "GET",
66
+ signal,
67
+ headers: {
68
+ "user-agent": USER_AGENT,
69
+ accept: "text/html,application/xhtml+xml",
70
+ "accept-encoding": "identity",
71
+ },
72
+ }, (res) => {
73
+ const status = typeof res.statusCode === "number" ? res.statusCode : 0;
74
+ const chunks = [];
75
+ let received = 0;
76
+ let stopped = false;
77
+ const stop = () => {
78
+ if (stopped)
79
+ return;
80
+ stopped = true;
81
+ try {
82
+ res.destroy();
83
+ }
84
+ catch {
85
+ // ignore — we are abandoning the socket deliberately
86
+ }
87
+ };
88
+ res.on("data", (chunk) => {
89
+ if (stopped)
90
+ return;
91
+ const remaining = MAX_RESPONSE_BYTES - received;
92
+ if (remaining <= 0) {
93
+ stop();
94
+ return;
95
+ }
96
+ if (chunk.byteLength > remaining) {
97
+ chunks.push(chunk.subarray(0, remaining));
98
+ received += remaining;
99
+ stop();
100
+ return;
101
+ }
102
+ chunks.push(chunk);
103
+ received += chunk.byteLength;
104
+ });
105
+ res.once("end", () => {
106
+ const body = Buffer.concat(chunks, received).toString("utf-8");
107
+ resolve({ status, body });
108
+ });
109
+ res.once("close", () => {
110
+ // If the body was truncated by `stop()`, `end` does not
111
+ // fire — resolve from `close` so the promise still
112
+ // settles.
113
+ if (stopped) {
114
+ const body = Buffer.concat(chunks, received).toString("utf-8");
115
+ resolve({ status, body });
116
+ }
117
+ });
118
+ res.once("error", (err) => {
119
+ reject(err);
120
+ });
121
+ });
122
+ }
123
+ catch (err) {
124
+ reject(err);
125
+ return;
126
+ }
127
+ req.once("error", (err) => {
128
+ reject(err);
129
+ });
130
+ req.end();
131
+ });
132
+ }
133
+ /**
134
+ * Disallowed character class for hit URLs (Requirement 7.3): any
135
+ * whitespace or ASCII control character. The handler in `web.search`
136
+ * applies the same filter, so dropping here is purely an early exit.
137
+ */
138
+ const URL_INVALID_CHARS_RE = /[\s\u0000-\u001f\u007f]/;
139
+ /**
140
+ * Validate a candidate hit URL against the rules `web.search` enforces:
141
+ * non-empty string, parseable as an absolute URL, scheme `http:` or
142
+ * `https:`, no whitespace, no ASCII control characters.
143
+ */
144
+ function isValidHitUrl(raw) {
145
+ if (typeof raw !== "string" || raw.length === 0)
146
+ return false;
147
+ if (URL_INVALID_CHARS_RE.test(raw))
148
+ return false;
149
+ let parsed;
150
+ try {
151
+ parsed = new URL(raw);
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
157
+ }
158
+ /**
159
+ * Strip DuckDuckGo's in-page redirect wrapper so the destination URL
160
+ * is what callers see.
161
+ *
162
+ * DDG renders result links as `/l/?uddg=<percent-encoded destination>`
163
+ * (sometimes with extra tracking parameters). For non-wrapper links —
164
+ * e.g. ad placements that point directly at an external URL — the
165
+ * input is returned as an absolute URL unchanged.
166
+ *
167
+ * Returns `undefined` when the input is empty, fails URL parsing, or
168
+ * is a `/l/` wrapper without a usable `uddg` parameter.
169
+ */
170
+ function unwrapDdgRedirect(href) {
171
+ if (typeof href !== "string" || href.length === 0)
172
+ return undefined;
173
+ let parsed;
174
+ try {
175
+ // Resolve protocol-relative (`//duckduckgo.com/l/…`) and absolute
176
+ // path (`/l/…`) forms against the DDG endpoint so URL parsing
177
+ // succeeds for every shape DDG emits.
178
+ parsed = new URL(href, ENDPOINT);
179
+ }
180
+ catch {
181
+ return undefined;
182
+ }
183
+ if (parsed.pathname === "/l/" && parsed.searchParams.has("uddg")) {
184
+ const destination = parsed.searchParams.get("uddg") ?? "";
185
+ if (destination.length === 0)
186
+ return undefined;
187
+ return destination;
188
+ }
189
+ return parsed.toString();
190
+ }
191
+ /**
192
+ * The DuckDuckGo {@link SearchProvider} adapter.
193
+ *
194
+ * Resolves a search query into a {@link RawProviderResponse} carrying
195
+ * up to `maxResults` `{title, url, snippet}` hits. URL filtering and
196
+ * `maxResults` truncation happen here so the registry handler does not
197
+ * have to re-walk the cheerio tree; the handler still re-applies the
198
+ * filter for defense in depth (Requirement 7.3).
199
+ */
200
+ export const duckduckgoProvider = {
201
+ id: "duckduckgo",
202
+ displayName: "DuckDuckGo",
203
+ needsApiKey: false,
204
+ async search(query, maxResults, _auth, signal) {
205
+ const url = `${ENDPOINT}?q=${encodeURIComponent(query)}`;
206
+ const { status, body } = await httpsGetText(url, signal);
207
+ // Non-2xx responses surface to the handler with an empty hit
208
+ // list; the handler maps the status to the right
209
+ // `WebSearchErrorKind` (auth/rate-limit/server/http) per
210
+ // Requirements 6.1, 6.2, 6.6, and 1.9.
211
+ if (status < 200 || status >= 300) {
212
+ return { status, hits: [] };
213
+ }
214
+ let $;
215
+ try {
216
+ $ = cheerio.load(body);
217
+ }
218
+ catch (err) {
219
+ return {
220
+ status,
221
+ hits: [],
222
+ parseError: err instanceof Error ? err.message : String(err),
223
+ };
224
+ }
225
+ const hits = [];
226
+ $(".result").each((_idx, el) => {
227
+ if (hits.length >= maxResults)
228
+ return false;
229
+ const titleAnchor = $(el).find(".result__title a").first();
230
+ const titleText = titleAnchor.text().trim();
231
+ const href = titleAnchor.attr("href") ?? "";
232
+ const destination = unwrapDdgRedirect(href);
233
+ // Drop hits whose URL is missing/invalid before they count
234
+ // toward `maxResults` (Requirement 7.3).
235
+ if (destination === undefined || !isValidHitUrl(destination))
236
+ return;
237
+ const snippet = $(el).find(".result__snippet").first().text().trim();
238
+ hits.push({ title: titleText, url: destination, snippet });
239
+ return;
240
+ });
241
+ return { status, hits };
242
+ },
243
+ };
244
+ // Register the adapter in the shared registry so the `web.search`
245
+ // handler can dispatch through it once `activeSearchProvider` is set
246
+ // to `"duckduckgo"`.
247
+ searchProviders.duckduckgo = duckduckgoProvider;
248
+ //# sourceMappingURL=duckduckgo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckduckgo.js","sourceRoot":"","sources":["../../../../src/tools/web/providers/duckduckgo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Search-provider adapter interface and registry stub.
3
+ *
4
+ * Concrete provider implementations (Brave, Tavily, DuckDuckGo) live next to
5
+ * this file and register themselves into {@link searchProviders}. The registry
6
+ * starts empty; it is populated by the per-provider modules so a missing
7
+ * import surfaces immediately as a `Cannot read property 'search'` style error
8
+ * rather than as a silently wrong dispatch.
9
+ *
10
+ * Shapes here match the design document's "Provider adapter interface"
11
+ * section verbatim.
12
+ */
13
+ import { type SearchProviderId } from "../types.js";
14
+ /**
15
+ * Adapter implemented by every search-provider module.
16
+ *
17
+ * The `search` method is called by `web.search` after argument validation and
18
+ * provider/key resolution. Implementations must:
19
+ *
20
+ * - Issue exactly one outbound HTTP request (no retry on transient failure;
21
+ * Requirement 6.7).
22
+ * - Honor the provided {@link AbortSignal} for the 15-second invocation
23
+ * timeout (Requirement 1.8).
24
+ * - Return a {@link RawProviderResponse} describing the HTTP outcome and the
25
+ * raw, unfiltered hit list. Shape validation, URL filtering, and
26
+ * `maxResults` truncation are performed by the `web.search` handler, not by
27
+ * the adapter.
28
+ */
29
+ export interface SearchProvider {
30
+ /** Stable identifier matching one of {@link SearchProviderId}. */
31
+ id: SearchProviderId;
32
+ /** Human-friendly name shown in CLI listings. */
33
+ displayName: string;
34
+ /** Whether the adapter requires an API key to dispatch a request. */
35
+ needsApiKey: boolean;
36
+ /**
37
+ * Environment variable the key store consults first when resolving this
38
+ * provider's API key (e.g. `"BRAVE_SEARCH_API_KEY"`). Omitted for keyless
39
+ * providers.
40
+ */
41
+ envVar?: string;
42
+ /**
43
+ * Dispatch a single search request.
44
+ *
45
+ * @param query Already-trimmed query string; length ∈ [1, 400].
46
+ * @param maxResults Already-clamped result count; ∈ [1, 20].
47
+ * @param auth Resolved credentials. `apiKey` is present iff
48
+ * {@link needsApiKey} is true and a key was found.
49
+ * @param signal Abort signal wired to the 15-second invocation timer.
50
+ */
51
+ search(query: string, maxResults: number, auth: {
52
+ apiKey?: string;
53
+ }, signal: AbortSignal): Promise<RawProviderResponse>;
54
+ }
55
+ /**
56
+ * Provider-agnostic view of a single dispatch's outcome.
57
+ *
58
+ * Adapters surface raw HTTP status + hit array here so the `web.search`
59
+ * handler can map status codes to {@link import("../types.js").WebSearchErrorKind}
60
+ * uniformly across providers (Requirements 6.1, 6.2, 6.5, 6.6, 1.9).
61
+ */
62
+ export interface RawProviderResponse {
63
+ /** HTTP status code returned by the provider's endpoint. */
64
+ status: number;
65
+ /**
66
+ * Hit list extracted from the provider response. Fields are optional
67
+ * because validation/filtering happens in the handler — the adapter is
68
+ * permitted to forward whatever the provider produced.
69
+ */
70
+ hits: Array<{
71
+ title?: string;
72
+ url?: string;
73
+ snippet?: string;
74
+ }>;
75
+ /**
76
+ * Populated when the provider returned a 2xx status but the body did not
77
+ * match the adapter's expected shape; surfaces as
78
+ * {@link import("../types.js").WebSearchErrorKind} `"parse"` (Requirement 6.5).
79
+ */
80
+ parseError?: string;
81
+ }
82
+ /**
83
+ * Registry of installed search-provider adapters, keyed by
84
+ * {@link SearchProviderId}.
85
+ *
86
+ * Starts empty. Concrete adapters in `./brave.ts`, `./tavily.ts`, and
87
+ * `./duckduckgo.ts` populate this object on module import; the
88
+ * `web.search` handler resolves the active provider through it.
89
+ */
90
+ export declare const searchProviders: Record<SearchProviderId, SearchProvider>;
91
+ /**
92
+ * Validate that an arbitrary string is one of the supported
93
+ * {@link SearchProviderId} values, returning the narrowed type.
94
+ *
95
+ * Mirrors `assertProvider` in `src/llm/provider.ts` so CLI surfaces such as
96
+ * `clai set <provider>` and `clai search-provider <id>` get the same error
97
+ * shape regardless of which keyspace the id belongs to (Requirement 3.7).
98
+ */
99
+ export declare function assertSearchProvider(value: string): SearchProviderId;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Search-provider adapter interface and registry stub.
3
+ *
4
+ * Concrete provider implementations (Brave, Tavily, DuckDuckGo) live next to
5
+ * this file and register themselves into {@link searchProviders}. The registry
6
+ * starts empty; it is populated by the per-provider modules so a missing
7
+ * import surfaces immediately as a `Cannot read property 'search'` style error
8
+ * rather than as a silently wrong dispatch.
9
+ *
10
+ * Shapes here match the design document's "Provider adapter interface"
11
+ * section verbatim.
12
+ */
13
+ import { searchProviderIds, } from "../types.js";
14
+ /**
15
+ * Registry of installed search-provider adapters, keyed by
16
+ * {@link SearchProviderId}.
17
+ *
18
+ * Starts empty. Concrete adapters in `./brave.ts`, `./tavily.ts`, and
19
+ * `./duckduckgo.ts` populate this object on module import; the
20
+ * `web.search` handler resolves the active provider through it.
21
+ */
22
+ export const searchProviders = {};
23
+ /**
24
+ * Validate that an arbitrary string is one of the supported
25
+ * {@link SearchProviderId} values, returning the narrowed type.
26
+ *
27
+ * Mirrors `assertProvider` in `src/llm/provider.ts` so CLI surfaces such as
28
+ * `clai set <provider>` and `clai search-provider <id>` get the same error
29
+ * shape regardless of which keyspace the id belongs to (Requirement 3.7).
30
+ */
31
+ export function assertSearchProvider(value) {
32
+ const normalized = value.trim().toLowerCase();
33
+ if (searchProviderIds.includes(normalized)) {
34
+ return normalized;
35
+ }
36
+ throw new Error(`Unsupported search provider "${value}". Supported providers: ${searchProviderIds.join(", ")}`);
37
+ }
38
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../../../src/tools/web/providers/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,iBAAiB,GAElB,MAAM,aAAa,CAAC;AAuErB;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAA8C,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAK,iBAAuC,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAClE,OAAO,UAA8B,CAAC;IACxC,CAAC;IACD,MAAM,IAAI,KAAK,CACb,gCAAgC,KAAK,2BAA2B,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC/F,CAAC;AACJ,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Tavily search-provider adapter for `web.search`.
3
+ *
4
+ * Implements the {@link SearchProvider} contract from `./provider.ts` and
5
+ * registers itself in the {@link searchProviders} registry on import. The
6
+ * adapter performs exactly one outbound HTTPS request per invocation
7
+ * (Requirement 6.7), forwards the caller-provided {@link AbortSignal} to
8
+ * the underlying transport so the 15-second `web.search` timeout is
9
+ * honored (Requirement 1.8), and returns a {@link RawProviderResponse}
10
+ * describing the HTTP outcome plus the raw hit list.
11
+ *
12
+ * Status-to-error-kind classification (`401/403 → auth`, `429 → rate-limit`,
13
+ * `5xx → server`, non-JSON → `parse`, other non-2xx → `http`) is the
14
+ * responsibility of the `web.search` handler; this adapter only exposes
15
+ * the raw HTTP `status` and the parsed (or `parseError`-flagged) hit list
16
+ * so that mapping can be applied uniformly across providers (Requirements
17
+ * 6.1, 6.2, 6.5, 6.6).
18
+ *
19
+ * Endpoint and request shape match the design's "Per-provider notes →
20
+ * Tavily" section (`.kiro/specs/web-search-and-fetch/design.md`):
21
+ *
22
+ * - POST `https://api.tavily.com/search`
23
+ * - Body: `{ api_key, query, max_results, search_depth: "basic" }`
24
+ * where `max_results` is clamped to `[1..20]` defensively.
25
+ * - Response: `{ results: [{ title, url, content }] }` mapped into
26
+ * `SearchResult { title, url, snippet }`.
27
+ */
28
+ import https from "node:https";
29
+ import { type SearchProvider } from "./provider.js";
30
+ /**
31
+ * Inject point for the underlying HTTPS transport so unit/property
32
+ * tests can drive the adapter without touching the network. The
33
+ * default mirrors the standard `node:https.request` signature.
34
+ *
35
+ * Kept module-private (not exported as a normal export) because the
36
+ * public adapter contract intentionally takes no transport argument —
37
+ * `web.search` always uses the default transport in production.
38
+ */
39
+ type HttpsRequestFn = typeof https.request;
40
+ /**
41
+ * Test-only seam: swap the HTTPS transport used by the adapter.
42
+ * Production callers never invoke this; tests use it to inject a
43
+ * stubbed `request` implementation that emits scripted responses.
44
+ */
45
+ export declare function __setTavilyHttpsRequestForTesting(fn: HttpsRequestFn | undefined): void;
46
+ /**
47
+ * Tavily adapter. Registered in {@link searchProviders} as a
48
+ * side-effect of importing this module — `web.search` resolves the
49
+ * active provider via the registry.
50
+ */
51
+ export declare const tavilyProvider: SearchProvider;
52
+ export {};