@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.
- package/README.md +32 -0
- package/dist/agent/runner.js +41 -3
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/providers.js +28 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/search-providers.d.ts +50 -0
- package/dist/commands/search-providers.js +134 -0
- package/dist/commands/search-providers.js.map +1 -0
- package/dist/commands/update.js +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/provider.js +9 -6
- package/dist/llm/provider.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.js +6 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +139 -113
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +40 -0
- package/dist/safety/classifier.js.map +1 -1
- package/dist/store/config.d.ts +5 -0
- package/dist/store/config.js +7 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/keys.d.ts +65 -0
- package/dist/store/keys.js +164 -28
- package/dist/store/keys.js.map +1 -1
- package/dist/tools/http.d.ts +12 -1
- package/dist/tools/http.js +8 -43
- package/dist/tools/http.js.map +1 -1
- package/dist/tools/registry.js +52 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +25 -0
- package/dist/tools/shell.js +155 -6
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/web/audit.d.ts +154 -0
- package/dist/tools/web/audit.js +147 -0
- package/dist/tools/web/audit.js.map +1 -0
- package/dist/tools/web/budget.d.ts +76 -0
- package/dist/tools/web/budget.js +187 -0
- package/dist/tools/web/budget.js.map +1 -0
- package/dist/tools/web/capture.d.ts +201 -0
- package/dist/tools/web/capture.js +380 -0
- package/dist/tools/web/capture.js.map +1 -0
- package/dist/tools/web/fetch-core.d.ts +66 -0
- package/dist/tools/web/fetch-core.js +1123 -0
- package/dist/tools/web/fetch-core.js.map +1 -0
- package/dist/tools/web/fetch.d.ts +42 -0
- package/dist/tools/web/fetch.js +115 -0
- package/dist/tools/web/fetch.js.map +1 -0
- package/dist/tools/web/providers/brave.d.ts +46 -0
- package/dist/tools/web/providers/brave.js +263 -0
- package/dist/tools/web/providers/brave.js.map +1 -0
- package/dist/tools/web/providers/duckduckgo.d.ts +47 -0
- package/dist/tools/web/providers/duckduckgo.js +248 -0
- package/dist/tools/web/providers/duckduckgo.js.map +1 -0
- package/dist/tools/web/providers/provider.d.ts +99 -0
- package/dist/tools/web/providers/provider.js +38 -0
- package/dist/tools/web/providers/provider.js.map +1 -0
- package/dist/tools/web/providers/tavily.d.ts +52 -0
- package/dist/tools/web/providers/tavily.js +285 -0
- package/dist/tools/web/providers/tavily.js.map +1 -0
- package/dist/tools/web/readable.d.ts +67 -0
- package/dist/tools/web/readable.js +248 -0
- package/dist/tools/web/readable.js.map +1 -0
- package/dist/tools/web/redact.d.ts +120 -0
- package/dist/tools/web/redact.js +155 -0
- package/dist/tools/web/redact.js.map +1 -0
- package/dist/tools/web/search.d.ts +51 -0
- package/dist/tools/web/search.js +389 -0
- package/dist/tools/web/search.js.map +1 -0
- package/dist/tools/web/ssrf-guard.d.ts +85 -0
- package/dist/tools/web/ssrf-guard.js +265 -0
- package/dist/tools/web/ssrf-guard.js.map +1 -0
- package/dist/tools/web/types.d.ts +331 -0
- package/dist/tools/web/types.js +71 -0
- package/dist/tools/web/types.js.map +1 -0
- package/dist/ui/keys.js +3 -2
- package/dist/ui/keys.js.map +1 -1
- package/dist/ui/spinner.js +87 -14
- package/dist/ui/spinner.js.map +1 -1
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,KAAK,MAAM,YAAY,CAAC;AAG/B,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAGnC,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAEhD,0DAA0D;AAC1D,MAAM,QAAQ,GAAG,mCAAmC,CAAC;AAErD,mDAAmD;AACnD,MAAM,UAAU,GAAG,qBAAqB,CAAC;AASzC,IAAI,cAAc,GAAmB,KAAK,CAAC,OAAO,CAAC;AAEnD;;;;GAIG;AACH,MAAM,UAAU,qCAAqC,CACnD,EAA8B;IAE9B,cAAc,GAAG,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAYrC;;;;;;;;;;;;;GAaG;AACH,SAAS,YAAY,CAAC,GAAW,EAAE,MAAmB;IACpD,OAAO,IAAI,OAAO,CAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAClD,IAAI,GAAkB,CAAC;QACvB,IAAI,CAAC;YACH,GAAG,GAAG,cAAc,CAClB,GAAG,EACH;gBACE,MAAM,EAAE,KAAK;gBACb,MAAM;gBACN,OAAO,EAAE;oBACP,YAAY,EAAE,UAAU;oBACxB,MAAM,EAAE,iCAAiC;oBACzC,iBAAiB,EAAE,UAAU;iBAC9B;aACF,EACD,CAAC,GAAoB,EAAE,EAAE;gBACvB,MAAM,MAAM,GACV,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC1D,MAAM,MAAM,GAAa,EAAE,CAAC;gBAC5B,IAAI,QAAQ,GAAG,CAAC,CAAC;gBACjB,IAAI,OAAO,GAAG,KAAK,CAAC;gBAEpB,MAAM,IAAI,GAAG,GAAS,EAAE;oBACtB,IAAI,OAAO;wBAAE,OAAO;oBACpB,OAAO,GAAG,IAAI,CAAC;oBACf,IAAI,CAAC;wBACH,GAAG,CAAC,OAAO,EAAE,CAAC;oBAChB,CAAC;oBAAC,MAAM,CAAC;wBACP,qDAAqD;oBACvD,CAAC;gBACH,CAAC,CAAC;gBAEF,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;oBAC/B,IAAI,OAAO;wBAAE,OAAO;oBACpB,MAAM,SAAS,GAAG,kBAAkB,GAAG,QAAQ,CAAC;oBAChD,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;wBACnB,IAAI,EAAE,CAAC;wBACP,OAAO;oBACT,CAAC;oBACD,IAAI,KAAK,CAAC,UAAU,GAAG,SAAS,EAAE,CAAC;wBACjC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;wBAC1C,QAAQ,IAAI,SAAS,CAAC;wBACtB,IAAI,EAAE,CAAC;wBACP,OAAO;oBACT,CAAC;oBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACnB,QAAQ,IAAI,KAAK,CAAC,UAAU,CAAC;gBAC/B,CAAC,CAAC,CAAC;gBAEH,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE;oBACnB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;oBAC/D,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC5B,CAAC,CAAC,CAAC;gBAEH,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE;oBACrB,wDAAwD;oBACxD,mDAAmD;oBACnD,WAAW;oBACX,IAAI,OAAO,EAAE,CAAC;wBACZ,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;wBAC/D,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;oBAC5B,CAAC;gBACH,CAAC,CAAC,CAAC;gBAEH,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;oBACxB,MAAM,CAAC,GAAG,CAAC,CAAC;gBACd,CAAC,CAAC,CAAC;YACL,CAAC,CACF,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,GAAG,CAAC,CAAC;YACZ,OAAO;QACT,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;YAC/B,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,oBAAoB,GAAG,yBAAyB,CAAC;AAEvD;;;;GAIG;AACH,SAAS,aAAa,CAAC,GAAW;IAChC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACjD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC;AACrE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACpE,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,kEAAkE;QAClE,8DAA8D;QAC9D,sCAAsC;QACtC,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,KAAK,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;QACjE,MAAM,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC1D,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/C,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAmB;IAChD,EAAE,EAAE,YAAY;IAChB,WAAW,EAAE,YAAY;IACzB,WAAW,EAAE,KAAK;IAClB,KAAK,CAAC,MAAM,CACV,KAAa,EACb,UAAkB,EAClB,KAA0B,EAC1B,MAAmB;QAEnB,MAAM,GAAG,GAAG,GAAG,QAAQ,MAAM,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;QAEzD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAEzD,6DAA6D;QAC7D,iDAAiD;QACjD,yDAAyD;QACzD,uCAAuC;QACvC,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;YAClC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAC9B,CAAC;QAED,IAAI,CAAqB,CAAC;QAC1B,IAAI,CAAC;YACH,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO;gBACL,MAAM;gBACN,IAAI,EAAE,EAAE;gBACR,UAAU,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aAC7D,CAAC;QACJ,CAAC;QAED,MAAM,IAAI,GAA8D,EAAE,CAAC;QAE3E,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE;YAC7B,IAAI,IAAI,CAAC,MAAM,IAAI,UAAU;gBAAE,OAAO,KAAK,CAAC;YAE5C,MAAM,WAAW,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,KAAK,EAAE,CAAC;YAC3D,MAAM,SAAS,GAAG,WAAW,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAE5C,2DAA2D;YAC3D,yCAAyC;YACzC,IAAI,WAAW,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC;gBAAE,OAAO;YAErE,MAAM,OAAO,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YACrE,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC,CAAC,CAAC;QAEH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;CACF,CAAC;AAEF,kEAAkE;AAClE,qEAAqE;AACrE,qBAAqB;AACrB,eAAe,CAAC,UAAU,GAAG,kBAAkB,CAAC"}
|
|
@@ -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 {};
|