pi-unsloth-webtools 0.9.0 → 0.10.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 +64 -45
- package/engines.ts +254 -267
- package/index.ts +16 -2
- package/lightpanda.ts +23 -0
- package/package.json +2 -4
- package/scripts/install-lightpanda.sh +93 -5
- package/settings.ts +57 -0
- package/tls-fetch.ts +18 -10
- package/web-fetch.ts +2 -2
- package/web-search.ts +19 -2
package/index.ts
CHANGED
|
@@ -10,7 +10,13 @@ import {
|
|
|
10
10
|
type FetchPageOutcome,
|
|
11
11
|
} from "./web-fetch.ts";
|
|
12
12
|
import { renderPageWithLightpanda as defaultRenderLocalPageText } from "./lightpanda.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
loadDefaultEngines,
|
|
15
|
+
loadDefaultEngineWeights,
|
|
16
|
+
loadDefaultFetchSettings,
|
|
17
|
+
loadDefaultMaxPerHost,
|
|
18
|
+
loadLightpandaSettings,
|
|
19
|
+
} from "./settings.ts";
|
|
14
20
|
|
|
15
21
|
function toolCallLine(theme: Theme, name: string, detail: string) {
|
|
16
22
|
const line = theme.fg("toolTitle", theme.bold(name)) + (detail ? ` ${theme.fg("accent", detail)}` : "");
|
|
@@ -179,12 +185,20 @@ export function createWebTools(deps: WebToolsDeps = {}) {
|
|
|
179
185
|
onUpdate?.({ content: [{ type: "text", text: "Searching the web..." }], details: {} });
|
|
180
186
|
const timeoutParam = positiveNumber(params.timeoutMs);
|
|
181
187
|
const searchCwd = (_ctx as ExtensionContext | undefined)?.cwd;
|
|
182
|
-
const
|
|
188
|
+
const searchSettings = await loadDefaultFetchSettings(searchCwd);
|
|
189
|
+
const searchEngines = await loadDefaultEngines(searchCwd);
|
|
190
|
+
const searchEngineWeights = await loadDefaultEngineWeights(searchCwd);
|
|
191
|
+
const searchMaxPerHost = await loadDefaultMaxPerHost(searchCwd);
|
|
192
|
+
const searchTimeoutMs = timeoutParam ?? searchSettings.timeoutMs ?? SEARCH_TIMEOUT_MS;
|
|
183
193
|
const text = await webSearch(params.query, {
|
|
184
194
|
signal: signal ?? undefined,
|
|
185
195
|
timeoutMs: searchTimeoutMs,
|
|
186
196
|
maxResults: positiveNumber(params.maxResults),
|
|
187
197
|
cwd: searchCwd,
|
|
198
|
+
transport: searchSettings.transport,
|
|
199
|
+
engines: searchEngines,
|
|
200
|
+
engineWeights: searchEngineWeights,
|
|
201
|
+
maxPerHost: searchMaxPerHost,
|
|
188
202
|
});
|
|
189
203
|
return { content: [{ type: "text", text }], details: {} };
|
|
190
204
|
},
|
package/lightpanda.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { spawn as spawnProcess } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { visibleChars } from "./html-to-md.ts";
|
|
3
6
|
import {
|
|
4
7
|
checkUrlAccess,
|
|
@@ -201,11 +204,31 @@ function withProvenance(prefixed: string, url: string): string {
|
|
|
201
204
|
return [...lines.slice(0, index), ...header, ...lines.slice(index)].join("\n");
|
|
202
205
|
}
|
|
203
206
|
|
|
207
|
+
function installedLauncher(): string | null {
|
|
208
|
+
let dataHome = process.env.XDG_DATA_HOME?.trim();
|
|
209
|
+
if (!dataHome) {
|
|
210
|
+
try {
|
|
211
|
+
const home = homedir();
|
|
212
|
+
if (!home) return null;
|
|
213
|
+
dataHome = join(home, ".local", "share");
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const dir = join(dataHome, "pi-unsloth-webtools", "lightpanda");
|
|
219
|
+
for (const name of ["lightpanda-run", "lightpanda"]) {
|
|
220
|
+
const candidate = join(dir, name);
|
|
221
|
+
if (existsSync(candidate)) return candidate;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
204
226
|
export function lightpandaBinary(options: LightpandaRenderOptions = {}): string {
|
|
205
227
|
return (
|
|
206
228
|
options.binaryPath ??
|
|
207
229
|
options.settings?.binaryPath ??
|
|
208
230
|
process.env.PI_LIGHTPANDA_BIN?.trim() ??
|
|
231
|
+
installedLauncher() ??
|
|
209
232
|
DEFAULT_BINARY
|
|
210
233
|
);
|
|
211
234
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-unsloth-webtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi extension: web_search and web_fetch tools that began as a port of the Unsloth Studio codebase and now diverge from it (multi-engine search, opt-in SSRF guard, HTML-to-Markdown extraction)",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -59,10 +59,8 @@
|
|
|
59
59
|
"test:unit": "vitest run --exclude **/smoke.test.ts",
|
|
60
60
|
"test:smoke": "vitest run test/smoke.test.ts",
|
|
61
61
|
"test:lightpanda": "vitest run test/lightpanda-live.test.ts",
|
|
62
|
-
"compare:fetch": "node scripts/fetch-compare.ts",
|
|
63
|
-
"compare:browsers": "node scripts/compare-browsers.ts",
|
|
64
62
|
"camoufox:warmup": "node scripts/camoufox-warmup.ts",
|
|
65
|
-
"
|
|
63
|
+
"engine:eval": "node scripts/engine-eval.ts",
|
|
66
64
|
"typecheck": "tsc --noEmit",
|
|
67
65
|
"check:package": "node -e \"const fs=require('fs');const pkg=require('./package.json');const listed=new Set(pkg.files);const bad=[];for(const f of pkg.files){if(!fs.existsSync(f))bad.push('missing file: '+f)}for(const f of fs.readdirSync('.').filter(f=>f.endsWith('.ts'))){if(!listed.has(f))bad.push('unlisted source: '+f)}if(bad.length){console.error(bad.join('\\n'));process.exit(1)}\"",
|
|
68
66
|
"prepublishOnly": "npm run typecheck && npm run lint && npm run test:unit && npm run check:package && npm run check:publint",
|
|
@@ -10,6 +10,7 @@ VERSION=""
|
|
|
10
10
|
FORCE=0
|
|
11
11
|
ALLOW_SHIM=1
|
|
12
12
|
PRINT_PATH=0
|
|
13
|
+
NO_CONFIGURE=0
|
|
13
14
|
|
|
14
15
|
usage() {
|
|
15
16
|
cat <<'EOF'
|
|
@@ -23,12 +24,15 @@ Usage: bash scripts/install-lightpanda.sh [options]
|
|
|
23
24
|
--force Reinstall even when a working binary is already present
|
|
24
25
|
--no-shim Fail instead of bundling a newer libc when the system glibc is too old
|
|
25
26
|
--print-path Print only the launcher path (for scripts and CI)
|
|
27
|
+
--no-configure Skip writing webRender.lightpandaPath into the global settings
|
|
26
28
|
-h, --help Show this message
|
|
27
29
|
|
|
28
30
|
On Linux, if the release needs a newer glibc than the system provides, the script
|
|
29
31
|
downloads Debian's libc6 for the current stable suite, extracts it into the install
|
|
30
|
-
directory, and writes a launcher shim that runs the binary with that libc.
|
|
31
|
-
`webRender.lightpandaPath`
|
|
32
|
+
directory, and writes a launcher shim that runs the binary with that libc.
|
|
33
|
+
The script also writes `webRender.lightpandaPath` into the global settings so the
|
|
34
|
+
extension picks the binary up with no further setup; `--no-configure` skips that,
|
|
35
|
+
and `--print-path` prints only the launcher path for scripts and CI.
|
|
32
36
|
EOF
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -39,6 +43,7 @@ for arg in "$@"; do
|
|
|
39
43
|
--force) FORCE=1 ;;
|
|
40
44
|
--no-shim) ALLOW_SHIM=0 ;;
|
|
41
45
|
--print-path) PRINT_PATH=1 ;;
|
|
46
|
+
--no-configure) NO_CONFIGURE=1 ;;
|
|
42
47
|
-h|--help) usage; exit 0 ;;
|
|
43
48
|
*) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;;
|
|
44
49
|
esac
|
|
@@ -100,11 +105,96 @@ mkdir -p "$DIR"
|
|
|
100
105
|
binary="$DIR/lightpanda"
|
|
101
106
|
shim="$DIR/lightpanda-run"
|
|
102
107
|
|
|
108
|
+
settings_file_for_agent() {
|
|
109
|
+
local agent_dir="${PI_CODING_AGENT_DIR:-${PI_AGENT_DIR:-$HOME/.pi/agent}}"
|
|
110
|
+
case "$agent_dir" in
|
|
111
|
+
"~") agent_dir="$HOME" ;;
|
|
112
|
+
"~/"*) agent_dir="$HOME/${agent_dir#\~/}" ;;
|
|
113
|
+
esac
|
|
114
|
+
printf '%s\n' "$agent_dir/settings.json"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
configure_renderer() {
|
|
118
|
+
local binary_path="$1"
|
|
119
|
+
local settings_file
|
|
120
|
+
local status
|
|
121
|
+
settings_file="$(settings_file_for_agent)"
|
|
122
|
+
if [ "$NO_CONFIGURE" = 1 ]; then
|
|
123
|
+
echo "settings: skipped (--no-configure); set PI_LIGHTPANDA_BIN or webRender.lightpandaPath to $binary_path"
|
|
124
|
+
return
|
|
125
|
+
fi
|
|
126
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
127
|
+
echo "settings: node not found; set PI_LIGHTPANDA_BIN=$binary_path or webRender.lightpandaPath yourself" >&2
|
|
128
|
+
return
|
|
129
|
+
fi
|
|
130
|
+
status="$(SETTINGS_FILE="$settings_file" LIGHTPANDA_PATH="$binary_path" node --input-type=module <<'NODE'
|
|
131
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
132
|
+
import { dirname } from "node:path";
|
|
133
|
+
|
|
134
|
+
const settingsFile = process.env.SETTINGS_FILE;
|
|
135
|
+
const binaryPath = process.env.LIGHTPANDA_PATH;
|
|
136
|
+
function fail(message) {
|
|
137
|
+
console.error(message);
|
|
138
|
+
process.exit(3);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let settings = {};
|
|
142
|
+
let mode;
|
|
143
|
+
if (existsSync(settingsFile)) {
|
|
144
|
+
try {
|
|
145
|
+
settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
146
|
+
} catch {
|
|
147
|
+
fail(`settings: keeping ${settingsFile}: not valid JSON`);
|
|
148
|
+
}
|
|
149
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
150
|
+
fail(`settings: keeping ${settingsFile}: not a JSON object`);
|
|
151
|
+
}
|
|
152
|
+
mode = statSync(settingsFile).mode & 0o777;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sections = [settings.unslothWebTools, settings.webRender].filter(
|
|
156
|
+
(section) => section && typeof section === "object" && !Array.isArray(section),
|
|
157
|
+
);
|
|
158
|
+
const alreadyConfigured = sections.some(
|
|
159
|
+
(section) =>
|
|
160
|
+
(typeof section.lightpandaPath === "string" && section.lightpandaPath.length > 0) ||
|
|
161
|
+
(Array.isArray(section.lightpandaCommand) && section.lightpandaCommand.length > 0),
|
|
162
|
+
);
|
|
163
|
+
if (alreadyConfigured) {
|
|
164
|
+
console.log("kept");
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (settings.webRender !== undefined && (!settings.webRender || typeof settings.webRender !== "object" || Array.isArray(settings.webRender))) {
|
|
169
|
+
fail(`settings: keeping ${settingsFile}: webRender is not a JSON object`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
settings.webRender = { ...(settings.webRender ?? {}), lightpandaPath: binaryPath };
|
|
173
|
+
const temporary = `${settingsFile}.tmp.${process.pid}`;
|
|
174
|
+
try {
|
|
175
|
+
mkdirSync(dirname(settingsFile), { recursive: true });
|
|
176
|
+
writeFileSync(temporary, JSON.stringify(settings, null, 2) + "\n", mode === undefined ? {} : { mode });
|
|
177
|
+
renameSync(temporary, settingsFile);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
rmSync(temporary, { force: true });
|
|
180
|
+
fail(`settings: keeping ${settingsFile}: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
|
+
}
|
|
182
|
+
console.log("configured");
|
|
183
|
+
NODE
|
|
184
|
+
)" || status=""
|
|
185
|
+
case "$status" in
|
|
186
|
+
configured) echo "settings: ${settings_file} now sets webRender.lightpandaPath" ;;
|
|
187
|
+
kept) echo "settings: ${settings_file} already configures a lightpanda path or command; left unchanged" ;;
|
|
188
|
+
*) echo "settings: could not update ${settings_file} automatically" >&2 ;;
|
|
189
|
+
esac
|
|
190
|
+
}
|
|
191
|
+
|
|
103
192
|
report_path() {
|
|
104
193
|
if [ "$PRINT_PATH" = 1 ]; then
|
|
105
194
|
printf '%s\n' "$1"
|
|
106
195
|
else
|
|
107
196
|
echo "already installed and working: $1" >&2
|
|
197
|
+
configure_renderer "$1"
|
|
108
198
|
fi
|
|
109
199
|
exit 0
|
|
110
200
|
}
|
|
@@ -200,7 +290,5 @@ fi
|
|
|
200
290
|
echo
|
|
201
291
|
echo "Lightpanda $installed_version installed"
|
|
202
292
|
echo " run this: $run_path"
|
|
203
|
-
echo " configure one of:"
|
|
204
|
-
echo " export PI_LIGHTPANDA_BIN=$run_path"
|
|
205
|
-
echo " settings.json: { \"webRender\": { \"lightpandaPath\": \"$run_path\" } }"
|
|
206
293
|
echo " verify: $run_path fetch --dump markdown --wait-until networkidle https://example.com/"
|
|
294
|
+
configure_renderer "$run_path"
|
package/settings.ts
CHANGED
|
@@ -50,6 +50,26 @@ function pickBoolean(data: Record<string, unknown>, paths: string[][]): boolean
|
|
|
50
50
|
return undefined;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function pickNumberRecord(data: Record<string, unknown>, paths: string[][]): Record<string, number> | undefined {
|
|
54
|
+
for (const path of paths) {
|
|
55
|
+
let cur: unknown = data;
|
|
56
|
+
for (const key of path) {
|
|
57
|
+
if (cur && typeof cur === "object" && !Array.isArray(cur)) cur = (cur as Record<string, unknown>)[key];
|
|
58
|
+
else {
|
|
59
|
+
cur = undefined;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!cur || typeof cur !== "object" || Array.isArray(cur)) continue;
|
|
64
|
+
const record: Record<string, number> = {};
|
|
65
|
+
for (const [key, value] of Object.entries(cur as Record<string, unknown>)) {
|
|
66
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) record[key] = value;
|
|
67
|
+
}
|
|
68
|
+
if (Object.keys(record).length) return record;
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
53
73
|
function pickStringArray(data: Record<string, unknown>, paths: string[][]): string[] | undefined {
|
|
54
74
|
for (const path of paths) {
|
|
55
75
|
let cur: unknown = data;
|
|
@@ -133,6 +153,21 @@ const LIGHTPANDA_COMMAND_PATHS: string[][] = [
|
|
|
133
153
|
["unslothWebTools", "lightpandaCommand"],
|
|
134
154
|
["webRender", "lightpandaCommand"],
|
|
135
155
|
];
|
|
156
|
+
|
|
157
|
+
const ENGINE_WEIGHTS_PATHS: string[][] = [
|
|
158
|
+
["unslothWebTools", "engineWeights"],
|
|
159
|
+
["webSearch", "engineWeights"],
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const MAX_PER_HOST_PATHS: string[][] = [
|
|
163
|
+
["unslothWebTools", "maxPerHost"],
|
|
164
|
+
["webSearch", "maxPerHost"],
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const ENGINE_PATHS: string[][] = [
|
|
168
|
+
["unslothWebTools", "engines"],
|
|
169
|
+
["webSearch", "engines"],
|
|
170
|
+
];
|
|
136
171
|
function clampMaxResults(value: number): number {
|
|
137
172
|
return Math.min(20, Math.max(1, value));
|
|
138
173
|
}
|
|
@@ -163,6 +198,28 @@ export async function loadDefaultMaxResults(cwd?: string): Promise<number> {
|
|
|
163
198
|
return result;
|
|
164
199
|
}
|
|
165
200
|
|
|
201
|
+
export async function loadDefaultEngines(cwd?: string): Promise<string[] | undefined> {
|
|
202
|
+
let result: string[] | undefined;
|
|
203
|
+
for (const data of await settingsEntries(cwd)) {
|
|
204
|
+
const candidate = pickStringArray(data, ENGINE_PATHS);
|
|
205
|
+
if (candidate !== undefined) result = candidate;
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function loadDefaultEngineWeights(cwd?: string): Promise<Record<string, number> | undefined> {
|
|
211
|
+
let result: Record<string, number> | undefined;
|
|
212
|
+
for (const data of await settingsEntries(cwd)) {
|
|
213
|
+
const candidate = pickNumberRecord(data, ENGINE_WEIGHTS_PATHS);
|
|
214
|
+
if (candidate !== undefined) result = candidate;
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function loadDefaultMaxPerHost(cwd?: string): Promise<number | undefined> {
|
|
220
|
+
return loadFetchSetting(cwd, MAX_PER_HOST_PATHS, (n) => n >= 0);
|
|
221
|
+
}
|
|
222
|
+
|
|
166
223
|
async function loadFetchSetting(cwd: string | undefined, paths: string[][], valid: (n: number) => boolean): Promise<number | undefined> {
|
|
167
224
|
let result: number | undefined;
|
|
168
225
|
for (const data of await settingsEntries(cwd)) {
|
package/tls-fetch.ts
CHANGED
|
@@ -49,15 +49,19 @@ interface ImpersonationModule {
|
|
|
49
49
|
|
|
50
50
|
export type ImpersonationLoader = () => Promise<ImpersonationModule | null>;
|
|
51
51
|
|
|
52
|
+
export type FetchTransport = "tls-first" | "direct-first" | "off";
|
|
53
|
+
|
|
52
54
|
export interface TlsHopOptions {
|
|
53
55
|
url: URL;
|
|
54
|
-
pinnedIp
|
|
55
|
-
family
|
|
56
|
+
pinnedIp?: string;
|
|
57
|
+
family?: number;
|
|
56
58
|
timeoutMs: number;
|
|
57
59
|
signal?: AbortSignal;
|
|
58
60
|
maxBytes: number;
|
|
59
61
|
maxPdfBytes?: number;
|
|
60
62
|
profile?: string;
|
|
63
|
+
method?: string;
|
|
64
|
+
body?: string;
|
|
61
65
|
extraHeaders?: Record<string, string>;
|
|
62
66
|
loader?: ImpersonationLoader;
|
|
63
67
|
}
|
|
@@ -85,7 +89,7 @@ async function loadImpersonationModule(): Promise<ImpersonationModule | null> {
|
|
|
85
89
|
const transports = new Map<string, Promise<ImpersonationTransport>>();
|
|
86
90
|
|
|
87
91
|
function transportKey(options: TlsHopOptions): string {
|
|
88
|
-
return `${options.profile ?? DEFAULT_PROFILE}|${options.url.hostname}|${options.pinnedIp}|${options.family}`;
|
|
92
|
+
return `${options.profile ?? DEFAULT_PROFILE}|${options.url.hostname}|${options.pinnedIp ?? ""}|${options.family ?? 0}`;
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
function evictTransports(): void {
|
|
@@ -107,12 +111,13 @@ async function transportFor(
|
|
|
107
111
|
const key = transportKey(options);
|
|
108
112
|
const existing = transports.get(key);
|
|
109
113
|
if (existing) return existing;
|
|
114
|
+
const transportOptions: Record<string, unknown> = {
|
|
115
|
+
browser: options.profile ?? DEFAULT_PROFILE,
|
|
116
|
+
os: DEFAULT_OS,
|
|
117
|
+
};
|
|
118
|
+
if (options.pinnedIp) transportOptions.resolve = { [options.url.hostname]: options.pinnedIp };
|
|
110
119
|
const created = module
|
|
111
|
-
.createTransport(
|
|
112
|
-
resolve: { [options.url.hostname]: options.pinnedIp },
|
|
113
|
-
browser: options.profile ?? DEFAULT_PROFILE,
|
|
114
|
-
os: DEFAULT_OS,
|
|
115
|
-
})
|
|
120
|
+
.createTransport(transportOptions)
|
|
116
121
|
.catch((error: unknown) => {
|
|
117
122
|
transports.delete(key);
|
|
118
123
|
throw error;
|
|
@@ -179,12 +184,15 @@ export async function impersonatedRequest(options: TlsHopOptions): Promise<TlsHo
|
|
|
179
184
|
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
180
185
|
let response: ImpersonationResponse;
|
|
181
186
|
try {
|
|
182
|
-
|
|
187
|
+
const init: Record<string, unknown> = {
|
|
183
188
|
transport,
|
|
189
|
+
method: options.method ?? "GET",
|
|
184
190
|
redirect: "manual",
|
|
185
191
|
signal,
|
|
186
192
|
headers: hopHeaders(options.extraHeaders),
|
|
187
|
-
}
|
|
193
|
+
};
|
|
194
|
+
if (options.body !== undefined) init.body = options.body;
|
|
195
|
+
response = await module.fetch(options.url.toString(), init);
|
|
188
196
|
} catch (error) {
|
|
189
197
|
throw new Error(failureMessage(error, options.signal, signal));
|
|
190
198
|
}
|
package/web-fetch.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
stripIpv6Brackets,
|
|
24
24
|
type WebsitePolicy,
|
|
25
25
|
} from "./web-access.ts";
|
|
26
|
-
import { impersonatedRequest, type TlsHopOptions, type TlsHopResponse } from "./tls-fetch.ts";
|
|
26
|
+
import { impersonatedRequest, type FetchTransport, type TlsHopOptions, type TlsHopResponse } from "./tls-fetch.ts";
|
|
27
27
|
import { collapseWhitespace, decodeHtmlEntities, feedHtml, htmlToMarkdown, visibleChars } from "./html-to-md.ts";
|
|
28
28
|
import type { AttrDict } from "./html-to-md.ts";
|
|
29
29
|
import { INVALID_CHARREFS } from "./entities.ts";
|
|
@@ -210,7 +210,7 @@ export interface ResolvedHost {
|
|
|
210
210
|
alternates?: { ip: string; family: number }[];
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
export type FetchTransport
|
|
213
|
+
export type { FetchTransport };
|
|
214
214
|
|
|
215
215
|
type TransportKind = "direct" | "tls";
|
|
216
216
|
export interface FetchSeams {
|
package/web-search.ts
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
EmptySweepError,
|
|
7
7
|
SearchCancelled,
|
|
8
8
|
SearchTimeoutError,
|
|
9
|
+
type SearchEngineOptions,
|
|
9
10
|
type SearchResult,
|
|
10
11
|
} from "./engines.ts";
|
|
12
|
+
import type { FetchTransport } from "./tls-fetch.ts";
|
|
11
13
|
|
|
12
14
|
export { EmptySweepError, SearchCancelled, SearchTimeoutError } from "./engines.ts";
|
|
13
15
|
|
|
@@ -24,15 +26,20 @@ export type SearchClient = (
|
|
|
24
26
|
maxResults: number,
|
|
25
27
|
signal?: AbortSignal,
|
|
26
28
|
timeoutMs?: number,
|
|
29
|
+
options?: SearchEngineOptions,
|
|
27
30
|
) => Promise<SearchResult[]>;
|
|
28
31
|
export async function ddgSearch(
|
|
29
32
|
query: string,
|
|
30
33
|
maxResults = MAX_RESULTS,
|
|
31
34
|
signal?: AbortSignal,
|
|
32
35
|
timeoutMs = SEARCH_TIMEOUT_MS,
|
|
36
|
+
options: SearchEngineOptions = {},
|
|
33
37
|
): Promise<SearchResult[]> {
|
|
34
38
|
if (signal?.aborted) throw new SearchCancelled();
|
|
35
|
-
return autoTextSearch(query, maxResults, timeoutMs, signal
|
|
39
|
+
return autoTextSearch(query, maxResults, timeoutMs, signal, {
|
|
40
|
+
...options,
|
|
41
|
+
transport: options.transport ?? "tls-first",
|
|
42
|
+
});
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
const POLICY_OVERFETCH = 4;
|
|
@@ -44,6 +51,10 @@ export interface WebSearchOptions {
|
|
|
44
51
|
websitePolicy?: WebsitePolicy | null;
|
|
45
52
|
client?: SearchClient;
|
|
46
53
|
cwd?: string;
|
|
54
|
+
transport?: FetchTransport;
|
|
55
|
+
engines?: string[];
|
|
56
|
+
engineWeights?: Record<string, number>;
|
|
57
|
+
maxPerHost?: number;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
export { loadDefaultMaxResults };
|
|
@@ -71,7 +82,13 @@ export async function webSearch(
|
|
|
71
82
|
(policy?.blockedDomains?.length ?? 0) > 0,
|
|
72
83
|
);
|
|
73
84
|
const wanted = restricted ? maxResults * POLICY_OVERFETCH : maxResults;
|
|
74
|
-
const results = await client(effectiveQuery, wanted, signal, timeoutMs
|
|
85
|
+
const results = await client(effectiveQuery, wanted, signal, timeoutMs, {
|
|
86
|
+
transport: options.transport,
|
|
87
|
+
policy,
|
|
88
|
+
engines: options.engines,
|
|
89
|
+
engineWeights: options.engineWeights,
|
|
90
|
+
maxPerHost: options.maxPerHost,
|
|
91
|
+
});
|
|
75
92
|
if (signal?.aborted) return "Search cancelled.";
|
|
76
93
|
if (!results.length) return EMPTY_SEARCH_RESULTS[0];
|
|
77
94
|
const allowed: SearchResult[] = [];
|