@browserwright/pi 0.18.1 → 0.18.3
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 +25 -11
- package/config.json +1 -1
- package/core/config.ts +3 -2
- package/core/format.ts +15 -1
- package/core/predicates.ts +8 -3
- package/core/results.ts +5 -0
- package/core/types.ts +18 -0
- package/index.ts +6 -3
- package/package.json +2 -2
- package/providers/browserwright-search.json +5 -1
- package/providers/browserwright-search.ts +15 -1
- package/providers/raw-text.ts +78 -0
- package/providers/raw.json +16 -0
package/README.md
CHANGED
|
@@ -4,12 +4,14 @@ Two tools for [pi](https://github.com/badlogic/pi-mono), backed by **declarative
|
|
|
4
4
|
providers** that drive [browserwright](https://github.com/broven/browserwright):
|
|
5
5
|
|
|
6
6
|
```
|
|
7
|
-
bw_web_fetch(url, provider?) → the page as Markdown
|
|
7
|
+
bw_web_fetch(url, provider?) → the page as Markdown, or raw text for text endpoints
|
|
8
8
|
bw_web_search(query, provider?) → ranked links + the SERP features Google showed
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
including pages behind a login.
|
|
11
|
+
The browserwright paths run through the user's **own Chrome**, so they see what
|
|
12
|
+
the user sees — including pages behind a login. Fetch also has a raw-text
|
|
13
|
+
fallback for endpoints such as GitHub Raw; that fallback makes a direct request
|
|
14
|
+
and returns the text body verbatim. Zero npm dependencies; `typebox` and the pi
|
|
13
15
|
packages come from pi's own install.
|
|
14
16
|
|
|
15
17
|
## Install
|
|
@@ -106,17 +108,20 @@ truncated: 382 of 480 lines (49.7KB of 71.5KB) · full: /tmp/browserwright-pi-xx
|
|
|
106
108
|
|
|
107
109
|
## What ships, and what does not
|
|
108
110
|
|
|
109
|
-
This package ships
|
|
111
|
+
This package ships the browserwright-backed rungs plus a text fallback for fetch.
|
|
112
|
+
There is one browserwright rung per tool:
|
|
110
113
|
|
|
111
114
|
| tool | provider | kind |
|
|
112
115
|
|------|----------|------|
|
|
113
|
-
| `bw_web_fetch` | `browserwright` | `command` — `
|
|
116
|
+
| `bw_web_fetch` | `browserwright` → `raw` | `command` — browser-rendered HTML, then `module` — text body verbatim |
|
|
114
117
|
| `bw_web_search` | `browserwright-search` | `module` — a session lifecycle in TS |
|
|
115
118
|
|
|
116
|
-
That is a real trade-off, and it points the wrong way for casual fetches:
|
|
117
|
-
`bw_web_fetch` opens a tab in the daily browser and takes ~4-7s,
|
|
118
|
-
reader API answers in ~1s without touching Chrome. What you get
|
|
119
|
-
state and full JS rendering
|
|
119
|
+
That is a real trade-off, and it points the wrong way for casual HTML fetches: a
|
|
120
|
+
browserwright `bw_web_fetch` opens a tab in the daily browser and takes ~4-7s,
|
|
121
|
+
where a hosted reader API answers in ~1s without touching Chrome. What you get
|
|
122
|
+
for the browser rung is login state and full JS rendering. Text endpoints such
|
|
123
|
+
as GitHub Raw skip the browser conversion failure and are returned verbatim by
|
|
124
|
+
the `raw` fallback.
|
|
120
125
|
|
|
121
126
|
**The chain engine is still here.** Drop your own JSON into `providers/` to add a
|
|
122
127
|
cheaper or anonymous rung ahead of the browser one — nothing needs to be
|
|
@@ -204,7 +209,8 @@ extraction has to run against the live DOM rather than the document response.
|
|
|
204
209
|
### `returns`
|
|
205
210
|
|
|
206
211
|
`markdown` | `html` | `text` | `results`. **The core never converts between
|
|
207
|
-
them**; it only labels the output so the model knows what it is reading.
|
|
212
|
+
them**; it only labels the output so the model knows what it is reading. The
|
|
213
|
+
built-in `raw` fetch provider returns accepted text response bodies unchanged.
|
|
208
214
|
|
|
209
215
|
## failWhen: the reason the chain exists
|
|
210
216
|
|
|
@@ -222,9 +228,17 @@ legitimately inside raw HTML and inside search results *about* JavaScript.
|
|
|
222
228
|
| field | applies to | note |
|
|
223
229
|
|-------|-----------|------|
|
|
224
230
|
| `minChars` | text payloads only | default 0 (off) |
|
|
225
|
-
| `minResults` | list payloads only | an empty list is rejected regardless |
|
|
231
|
+
| `minResults` | list payloads only | an empty list is rejected regardless, unless the engine asserted it |
|
|
226
232
|
| `matches` | both | searched in the text, or in joined titles + snippets |
|
|
227
233
|
|
|
234
|
+
The one exception is an **asserted** empty. When a search provider reports
|
|
235
|
+
`noMatch: true` — Google says "did not match any documents" in prose, on an
|
|
236
|
+
ordinary HTTP 200 page — the empty list is the engine's answer and is accepted,
|
|
237
|
+
`minResults` included. Everything else empty is still rejected, because that is
|
|
238
|
+
what a consent wall or a captcha looks like from here. Without the distinction a
|
|
239
|
+
query the engine answered correctly reaches the model as `bw_web_search failed`,
|
|
240
|
+
which reads as broken tooling rather than as a query worth rewriting.
|
|
241
|
+
|
|
228
242
|
`minChars` is deliberately not applied to a list, and `minResults` not to text:
|
|
229
243
|
the two floors measure different things, and applying both would reject a short
|
|
230
244
|
but complete set of hits.
|
package/config.json
CHANGED
package/core/config.ts
CHANGED
|
@@ -24,13 +24,14 @@ const LOG_PREFIX = "[browserwright-pi]";
|
|
|
24
24
|
|
|
25
25
|
const DEFAULT_CONFIG: PiConfig = {
|
|
26
26
|
order: {
|
|
27
|
-
fetch: ["browserwright"],
|
|
27
|
+
fetch: ["browserwright", "raw"],
|
|
28
28
|
search: ["browserwright-search"],
|
|
29
29
|
},
|
|
30
30
|
// The default line of defence. minChars stays 0 on purpose: a false positive
|
|
31
31
|
// escalates to a rung that opens a tab in the user's real Chrome, so
|
|
32
32
|
// over-eager rejection interrupts them. Per-provider thresholds are meant to
|
|
33
|
-
// come from `/bw probe` evidence, not from guesses.
|
|
33
|
+
// come from `/bw probe` evidence, not from guesses. The raw text rung opts out
|
|
34
|
+
// of the phrase list because source files may legitimately contain those words.
|
|
34
35
|
defaultFailWhen: {
|
|
35
36
|
minChars: 0,
|
|
36
37
|
minResults: 0,
|
package/core/format.ts
CHANGED
|
@@ -154,6 +154,20 @@ export function renderResults(result: ChainResult<SearchPayload>, query: string)
|
|
|
154
154
|
if (lines.length > 0) out.push("", "## Knowledge panel", ...lines);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
if (results.length === 0) {
|
|
158
|
+
// Reaching the success path with no rows means the engine said so itself
|
|
159
|
+
// (see SearchPayload.noMatch) — everything else is rejected upstream. Say
|
|
160
|
+
// which it was, and say what to do next: the model's move here is to
|
|
161
|
+
// rewrite the query, not to conclude the tool is broken.
|
|
162
|
+
out.push(
|
|
163
|
+
"",
|
|
164
|
+
payload.noMatch
|
|
165
|
+
? "The search engine states that nothing matched this query. That is its answer, not a failure — " +
|
|
166
|
+
"widen the query before retrying: drop a `site:` path, a quoted phrase, or the least essential keywords."
|
|
167
|
+
: "No rows were extracted from the results page.",
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
157
171
|
if (results.length > 0) {
|
|
158
172
|
out.push("");
|
|
159
173
|
for (const row of results) {
|
|
@@ -171,7 +185,7 @@ export function renderResults(result: ChainResult<SearchPayload>, query: string)
|
|
|
171
185
|
out.push("## Related searches", payload.relatedSearches.join(" · "), "");
|
|
172
186
|
}
|
|
173
187
|
|
|
174
|
-
out.push("Use bw_web_fetch on a URL above to read it.");
|
|
188
|
+
if (results.length > 0) out.push("Use bw_web_fetch on a URL above to read it.");
|
|
175
189
|
return out.join("\n");
|
|
176
190
|
}
|
|
177
191
|
|
package/core/predicates.ts
CHANGED
|
@@ -74,6 +74,7 @@ export const inspectText: Inspector<string> = (content) => ({ text: content });
|
|
|
74
74
|
export const inspectSearch: Inspector<SearchPayload> = (payload) => ({
|
|
75
75
|
count: payload.results.length,
|
|
76
76
|
text: payload.results.map((r) => `${r.title} ${r.snippet ?? ""}`).join("\n"),
|
|
77
|
+
authoritativeEmpty: payload.noMatch === true,
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
/**
|
|
@@ -89,9 +90,13 @@ export function failureReason<T>(value: T, rule: FailWhen, inspect: Inspector<T>
|
|
|
89
90
|
const count = inspected.count;
|
|
90
91
|
|
|
91
92
|
if (count !== undefined) {
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
93
|
+
// An engine that said "no documents matched" has answered the question.
|
|
94
|
+
// Accepting that is what stops a correctly-answered query from reaching
|
|
95
|
+
// the model as a failed tool call; the needle scan below is skipped with
|
|
96
|
+
// it, which costs nothing because the haystack is empty either way.
|
|
97
|
+
if (count === 0 && inspected.authoritativeEmpty) return undefined;
|
|
98
|
+
// Any other empty list is a failure worth falling through on: an
|
|
99
|
+
// interstitial usually yields a valid, empty list rather than an error.
|
|
95
100
|
if (count === 0) return "no results";
|
|
96
101
|
const minResults = rule.minResults ?? 0;
|
|
97
102
|
if (minResults > 0 && count < minResults) {
|
package/core/results.ts
CHANGED
|
@@ -20,6 +20,7 @@ const ANSWER_KEYS = ["answerBox", "answer_box", "answer", "featured_snippet"] as
|
|
|
20
20
|
const KG_KEYS = ["knowledgeGraph", "knowledge_graph"] as const;
|
|
21
21
|
const PAA_KEYS = ["peopleAlsoAsk", "people_also_ask", "relatedQuestions"] as const;
|
|
22
22
|
const RELATED_KEYS = ["relatedSearches", "related_searches", "relatedQueries"] as const;
|
|
23
|
+
const NO_MATCH_KEYS = ["noMatch", "no_match", "zeroResults"] as const;
|
|
23
24
|
|
|
24
25
|
function firstValue(row: Record<string, unknown>, keys: readonly string[]): unknown {
|
|
25
26
|
for (const key of keys) {
|
|
@@ -137,5 +138,9 @@ export function normalizeSearchPayload(value: unknown): SearchPayload {
|
|
|
137
138
|
const relatedSearches = stringList(firstValue(body, RELATED_KEYS));
|
|
138
139
|
if (relatedSearches) payload.relatedSearches = relatedSearches;
|
|
139
140
|
|
|
141
|
+
// Only meaningful next to an empty list. A source claiming both rows and
|
|
142
|
+
// "nothing matched" is contradicting itself, and the rows are the evidence.
|
|
143
|
+
if (payload.results.length === 0 && firstValue(body, NO_MATCH_KEYS) === true) payload.noMatch = true;
|
|
144
|
+
|
|
140
145
|
return payload;
|
|
141
146
|
}
|
package/core/types.ts
CHANGED
|
@@ -60,6 +60,18 @@ export interface KnowledgeGraph {
|
|
|
60
60
|
*/
|
|
61
61
|
export interface SearchPayload {
|
|
62
62
|
results: SearchResult[];
|
|
63
|
+
/**
|
|
64
|
+
* The engine stated, in so many words, that nothing matched.
|
|
65
|
+
*
|
|
66
|
+
* Only ever set alongside an empty `results`, and it is the one thing that
|
|
67
|
+
* separates an honestly empty search from the failure mode that looks
|
|
68
|
+
* identical from the outside: a captcha or consent wall parses perfectly and
|
|
69
|
+
* also yields zero rows. Without this flag both have to be rejected, and a
|
|
70
|
+
* query the engine answered correctly is reported to the model as a broken
|
|
71
|
+
* tool — which is exactly what it then works around instead of rewriting the
|
|
72
|
+
* query.
|
|
73
|
+
*/
|
|
74
|
+
noMatch?: boolean;
|
|
63
75
|
answerBox?: AnswerBox;
|
|
64
76
|
knowledgeGraph?: KnowledgeGraph;
|
|
65
77
|
peopleAlsoAsk?: string[];
|
|
@@ -236,6 +248,12 @@ export interface Inspected {
|
|
|
236
248
|
text: string;
|
|
237
249
|
/** Item count, for list payloads. Undefined for text payloads. */
|
|
238
250
|
count?: number;
|
|
251
|
+
/**
|
|
252
|
+
* A zero count that the source asserted rather than one we inferred from a
|
|
253
|
+
* payload we could not read. Lifts the blanket rejection of an empty list —
|
|
254
|
+
* see `SearchPayload.noMatch`.
|
|
255
|
+
*/
|
|
256
|
+
authoritativeEmpty?: boolean;
|
|
239
257
|
}
|
|
240
258
|
|
|
241
259
|
export type Inspector<T> = (value: T) => Inspected;
|
package/index.ts
CHANGED
|
@@ -58,14 +58,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
58
58
|
name: "bw_web_fetch",
|
|
59
59
|
label: "Fetch Web Page",
|
|
60
60
|
description:
|
|
61
|
-
"Fetch a URL and return its content as Markdown. " +
|
|
61
|
+
"Fetch a URL and return its content as Markdown or text. " +
|
|
62
62
|
`Tries providers in order until one returns usable content: ${config.order.fetch.join(" → ")}. ` +
|
|
63
63
|
"The response header states which provider answered and what format the body is in. " +
|
|
64
64
|
"Output over 50KB is truncated and the full text written to a temp file whose path is given.",
|
|
65
|
-
promptSnippet: "Fetch a URL as
|
|
65
|
+
promptSnippet: "Fetch a URL as Markdown or text, through the user's real browser",
|
|
66
66
|
promptGuidelines: [
|
|
67
67
|
"Prefer `bw_web_fetch` over curl or a shell HTTP client for reading web pages — it renders JavaScript " +
|
|
68
|
-
"and carries the user's login state,
|
|
68
|
+
"and carries the user's login state, while its text fallback also reads raw source endpoints.",
|
|
69
69
|
],
|
|
70
70
|
parameters: Type.Object({
|
|
71
71
|
url: Type.String({ description: "HTTP(S) URL to fetch" }),
|
|
@@ -180,6 +180,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
180
180
|
query,
|
|
181
181
|
provider: result.provider,
|
|
182
182
|
count: result.content?.results.length ?? 0,
|
|
183
|
+
// Zero rows is a success now, so the trace has to record which
|
|
184
|
+
// kind of zero it was.
|
|
185
|
+
noMatch: result.content?.noMatch === true,
|
|
183
186
|
features: {
|
|
184
187
|
answerBox: Boolean(result.content?.answerBox),
|
|
185
188
|
knowledgeGraph: Boolean(result.content?.knowledgeGraph),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserwright/pi",
|
|
3
|
-
"version": "0.18.
|
|
4
|
-
"description": "bw_web_fetch and bw_web_search for the pi coding agent,
|
|
3
|
+
"version": "0.18.3",
|
|
4
|
+
"description": "bw_web_fetch and bw_web_search for the pi coding agent, using browserwright plus a raw text fallback",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|
|
@@ -31,7 +31,11 @@
|
|
|
31
31
|
"them as a reason.",
|
|
32
32
|
"minResults 1 is the real guard. A captcha or consent wall parses perfectly",
|
|
33
33
|
"and yields an empty list, which is the failure mode that would otherwise be",
|
|
34
|
-
"indistinguishable from an honestly empty search.",
|
|
34
|
+
"indistinguishable from an honestly empty search. The extractor tells the two",
|
|
35
|
+
"apart by reading Google's own sentence ('did not match any documents') off",
|
|
36
|
+
"the page and setting noMatch, which lifts minResults for that one case: an",
|
|
37
|
+
"engine that says nothing matched has answered the query, and reporting that",
|
|
38
|
+
"as a tool failure only teaches the model to route around a working tool.",
|
|
35
39
|
"searchUrl is a template so the engine can be swapped without touching code.",
|
|
36
40
|
"{queryEncoded} and {limit} are the only substitutions.",
|
|
37
41
|
"SERP features (measured 2026-08-10, see docs/adr/0008). The page is server-",
|
|
@@ -246,6 +246,17 @@ function buildScript(query: string, limit: number, outPath: string, searchUrl: s
|
|
|
246
246
|
return out.slice(0, 10);
|
|
247
247
|
}, []);
|
|
248
248
|
|
|
249
|
+
// --- explicit "nothing matched" ---------------------------------------
|
|
250
|
+
// Google says this in prose on an otherwise ordinary page: no error, no
|
|
251
|
+
// interstitial, HTTP 200, zero rows. Reading the sentence is the only way to
|
|
252
|
+
// tell that empty list apart from the one a consent wall produces. Measured
|
|
253
|
+
// 2026-09-02 on a site:-with-path query, which is the shape that reaches zero
|
|
254
|
+
// most often.
|
|
255
|
+
const noMatch = attempt(() => {
|
|
256
|
+
const body = (document.body.innerText || '').toLowerCase();
|
|
257
|
+
return ['did not match any documents', 'no results found for'].some((n) => body.includes(n));
|
|
258
|
+
}, false);
|
|
259
|
+
|
|
249
260
|
// --- related searches --------------------------------------------------
|
|
250
261
|
// Scoped to #botstuff: the same href pattern at the top of the page is
|
|
251
262
|
// Google's own tab bar ("Images", "News", "Past hour"), not a related query.
|
|
@@ -266,7 +277,7 @@ function buildScript(query: string, limit: number, outPath: string, searchUrl: s
|
|
|
266
277
|
return out.slice(0, 10);
|
|
267
278
|
}, []);
|
|
268
279
|
|
|
269
|
-
return { results, answerBox, knowledgeGraph, peopleAlsoAsk, relatedSearches };
|
|
280
|
+
return { results, answerBox, knowledgeGraph, peopleAlsoAsk, relatedSearches, noMatch };
|
|
270
281
|
}`;
|
|
271
282
|
|
|
272
283
|
// json.dumps gives us correctly escaped Python string literals for free, so
|
|
@@ -310,6 +321,9 @@ function buildScript(query: string, limit: number, outPath: string, searchUrl: s
|
|
|
310
321
|
' "knowledgeGraph": data.get("knowledgeGraph"),',
|
|
311
322
|
' "peopleAlsoAsk": data.get("peopleAlsoAsk") or [],',
|
|
312
323
|
' "relatedSearches": data.get("relatedSearches") or [],',
|
|
324
|
+
// Only ever consulted when rows is empty, but carried unconditionally so
|
|
325
|
+
// the shape of the payload does not depend on the outcome.
|
|
326
|
+
' "noMatch": bool(data.get("noMatch")),',
|
|
313
327
|
" }",
|
|
314
328
|
" if not rows:",
|
|
315
329
|
// An interstitial parses fine and yields zero rows; say which kind it was.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The raw text fetch rung.
|
|
3
|
+
*
|
|
4
|
+
* `browserwright markdown` deliberately only converts HTML. Public source
|
|
5
|
+
* endpoints such as raw.githubusercontent.com return the source as text/plain,
|
|
6
|
+
* so they need a reader that returns the response body verbatim instead of
|
|
7
|
+
* trying to turn a browser's non-HTML document into Markdown.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ModuleContext, ProviderOutcome } from "../core/types.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* MIME types whose response body is safe to hand back as text.
|
|
14
|
+
*
|
|
15
|
+
* `text/*` covers source files served as text/plain. The application types
|
|
16
|
+
* cover APIs and source/document formats that are commonly served without a
|
|
17
|
+
* text/* MIME type. Binary responses such as application/pdf and
|
|
18
|
+
* application/octet-stream are intentionally not decoded here.
|
|
19
|
+
*/
|
|
20
|
+
const TEXT_APPLICATION_TYPES = new Set([
|
|
21
|
+
"application/graphql",
|
|
22
|
+
"application/javascript",
|
|
23
|
+
"application/json",
|
|
24
|
+
"application/ld+json",
|
|
25
|
+
"application/manifest+json",
|
|
26
|
+
"application/sql",
|
|
27
|
+
"application/toml",
|
|
28
|
+
"application/typescript",
|
|
29
|
+
"application/xml",
|
|
30
|
+
"application/x-javascript",
|
|
31
|
+
"application/x-yaml",
|
|
32
|
+
"application/yaml",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export function isTextContentType(raw: string | null | undefined): boolean {
|
|
36
|
+
const contentType = (raw ?? "").split(";", 1)[0].trim().toLowerCase();
|
|
37
|
+
return contentType.startsWith("text/") || TEXT_APPLICATION_TYPES.has(contentType);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default async function rawText(
|
|
41
|
+
subject: string,
|
|
42
|
+
ctx: ModuleContext,
|
|
43
|
+
): Promise<ProviderOutcome<string>> {
|
|
44
|
+
let response: Response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(subject, {
|
|
47
|
+
redirect: "follow",
|
|
48
|
+
signal: ctx.signal,
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (ctx.signal?.aborted) return { ok: false, reason: "aborted" };
|
|
52
|
+
return { ok: false, reason: `fetch failed: ${(error as Error).message}` };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
56
|
+
if (!isTextContentType(contentType)) {
|
|
57
|
+
const type = contentType.split(";", 1)[0].trim() || "unknown";
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
status: response.status,
|
|
61
|
+
reason: `not a text response (Content-Type: ${type})`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let text: string;
|
|
66
|
+
try {
|
|
67
|
+
text = await response.text();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { ok: false, status: response.status, reason: `could not read response: ${(error as Error).message}` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const hint = text.slice(0, 200).replace(/\s+/g, " ").trim();
|
|
74
|
+
return { ok: false, status: response.status, reason: `http ${response.status}${hint ? `: ${hint}` : ""}` };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: true, content: text, status: response.status };
|
|
78
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "raw",
|
|
3
|
+
"role": "fetch",
|
|
4
|
+
"kind": "module",
|
|
5
|
+
"module": "./providers/raw-text.ts",
|
|
6
|
+
"returns": "text",
|
|
7
|
+
"timeoutMs": 30000,
|
|
8
|
+
"failWhen": {
|
|
9
|
+
"matches": []
|
|
10
|
+
},
|
|
11
|
+
"_note": [
|
|
12
|
+
"Returns text responses verbatim for endpoints such as raw.githubusercontent.com.",
|
|
13
|
+
"The browserwright provider remains first, so HTML gets browser rendering and the user's login state when available.",
|
|
14
|
+
"Binary responses are rejected rather than decoded as corrupt text."
|
|
15
|
+
]
|
|
16
|
+
}
|