pi-unsloth-webtools 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -26
- package/lightpanda.ts +23 -0
- package/package.json +1 -4
- package/scripts/install-lightpanda.sh +93 -5
package/README.md
CHANGED
|
@@ -172,9 +172,10 @@ third-party rendering service.
|
|
|
172
172
|
still gets its chance.
|
|
173
173
|
- Lightpanda identifies itself honestly and refuses to impersonate a browser user agent, so hard
|
|
174
174
|
anti-bot walls are returned as failures (the direct error, or the incomplete-content note).
|
|
175
|
-
- Binary resolution: `
|
|
176
|
-
`
|
|
177
|
-
build) and macOS, plus Docker images; Windows
|
|
175
|
+
- Binary resolution: `webRender.lightpandaPath`, then `PI_LIGHTPANDA_BIN`, then the launcher
|
|
176
|
+
installed by `scripts/install-lightpanda.sh`, then `lightpanda` on `PATH`. Prebuilt binaries
|
|
177
|
+
exist for Linux (glibc; musl needs a source build) and macOS, plus Docker images; Windows
|
|
178
|
+
needs WSL2.
|
|
178
179
|
- Version matters: 1.0.0 renders JavaScript-heavy pages that the 0.2.x line cannot — measured on
|
|
179
180
|
the same machine, IMDb went from a 76-byte empty document to 21k characters, dribbble from 32
|
|
180
181
|
characters to 18k, and a Medium article from a challenge page to real text. Linux builds from
|
|
@@ -186,7 +187,9 @@ third-party rendering service.
|
|
|
186
187
|
- `bash scripts/install-lightpanda.sh` installs the newest stable release. When the system glibc
|
|
187
188
|
predates what the binary needs, it downloads Debian's `libc6` for the current stable suite,
|
|
188
189
|
extracts it into the install directory, and writes a launcher shim — so 1.0.0 runs on a
|
|
189
|
-
glibc 2.36 host without touching the system libraries.
|
|
190
|
+
glibc 2.36 host without touching the system libraries. It also writes
|
|
191
|
+
`webRender.lightpandaPath` into the global settings, so the extension picks the binary up with
|
|
192
|
+
no further setup; `--no-configure` skips that and `--print-path` prints the launcher path for CI.
|
|
190
193
|
- Disable the tier with `webRender.lightpandaEnabled: false`; `web_fetch` then stops after the
|
|
191
194
|
network attempts and reports the original error.
|
|
192
195
|
|
|
@@ -309,7 +312,7 @@ Optional settings in `~/.pi/agent/settings.json` or `.pi/settings.json` (project
|
|
|
309
312
|
| `unslothWebTools.allowLocalFiles` / `webFetch.allowLocalFiles` | `true` | Opt out to refuse local files in `web_fetch` (`file://` URLs, absolute, `~/`, or `./` paths); when enabled, PDFs are extracted and HTML converted |
|
|
310
313
|
| `webFetch.transport` / `unslothWebTools.transport` | `tls-first` | Fetch transport order: `tls-first` (default), `direct-first`, or `off` to disable the browser-fingerprint transport entirely |
|
|
311
314
|
| `webRender.lightpandaEnabled` / `unslothWebTools.lightpandaEnabled` | `true` | Opt out to disable local Lightpanda rendering |
|
|
312
|
-
| `webRender.lightpandaPath` / `unslothWebTools.lightpandaPath` | `lightpanda` on `PATH`
|
|
315
|
+
| `webRender.lightpandaPath` / `unslothWebTools.lightpandaPath` | launcher installed by `scripts/install-lightpanda.sh`, else `lightpanda` on `PATH` | Path to the Lightpanda binary used for local rendering |
|
|
313
316
|
| `webRender.lightpandaCommand` / `unslothWebTools.lightpandaCommand` | none | Command prefix that launches the renderer, for WSL (`["wsl.exe","-e","<path>"]`) or containers; overrides `lightpandaPath`. The fetch flags are appended to it |
|
|
314
317
|
|
|
315
318
|
Environment overrides: `PI_UNSLOTH_CACHE_DIR` changes the fetch cache directory, `PI_UNSLOTH_WEBTOOLS_STATS` opts into append-only sweep stats JSONL, `PI_CODING_AGENT_DIR` / `PI_AGENT_DIR` change the global settings directory, and `PI_LIGHTPANDA_BIN` points at the local renderer binary. Cache entries live 1 hour and stale copies are served only after a network failure. SOCKS5 proxies named by `HTTPS_PROXY`, `HTTP_PROXY`, or `ALL_PROXY` are honored on every fetch (`NO_PROXY` exclusions apply).
|
|
@@ -354,30 +357,9 @@ npm test
|
|
|
354
357
|
npm run test:unit
|
|
355
358
|
npm run test:smoke
|
|
356
359
|
bash scripts/install-lightpanda.sh
|
|
357
|
-
npm run compare:fetch
|
|
358
|
-
npm run compare:browsers
|
|
359
360
|
npm run camoufox:warmup
|
|
360
|
-
npm run stealth:matrix
|
|
361
|
-
/tmp/pyenv/bin/python scripts/stealth-python.py
|
|
362
361
|
```
|
|
363
362
|
|
|
364
|
-
`npm run compare:fetch` runs a live head-to-head of the direct fetch, the TLS-impersonation retry,
|
|
365
|
-
local Lightpanda rendering over a target list. It accepts URLs as arguments and `--no-lightpanda`
|
|
366
|
-
to drop the render tier. `npm run compare:browsers` adds a Camoufox column;
|
|
367
|
-
install it separately (`npm i camoufox-js playwright-core && npx camoufox-js fetch`, plus GTK3
|
|
368
|
-
libraries on Linux) and use `--seconds=N` to bound how long it waits out a JS challenge.
|
|
369
|
-
`npm run stealth:matrix` compares stealth-browser options against one walled page (plus
|
|
370
|
-
`bot.sannysoft.com` detection rows and a plain-page sanity check): raw CDP to a system Chromium
|
|
371
|
-
(`PI_CHROMIUM_BIN` to point at it), Playwright with its bundled Chromium, Patchright, and Camoufox.
|
|
372
|
-
Every browser dependency is loaded through a guarded dynamic import, so nothing is added to
|
|
373
|
-
`package.json`; install whichever rows you want to measure. `--attempts=N` and `--seconds=N` bound
|
|
374
|
-
the walled-page attempts, and passing row names runs a subset (`raw-cdp`, `playwright`, `patchright`,
|
|
375
|
-
`camoufox`).
|
|
376
|
-
|
|
377
|
-
`scripts/stealth-python.py` is the same idea for the Python-side options (nodriver, CloakBrowser,
|
|
378
|
-
DrissionPage, cloudscraper, curl_cffi) against the same walled page; it needs a venv with those
|
|
379
|
-
packages installed and is not wired into any npm script. Measured findings are in its module docstring.
|
|
380
|
-
|
|
381
363
|
`npm run camoufox:warmup` measures what a warm Camoufox costs and buys: launch time, idle CPU and
|
|
382
364
|
RSS, per-fetch latency with the browser already running, and whether a persistent profile
|
|
383
365
|
(`user_data_dir`, pinned fingerprint) lets a Cloudflare clearance survive a restart. Flags:
|
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.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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,7 @@
|
|
|
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
|
-
"stealth:matrix": "node scripts/stealth-matrix.ts",
|
|
66
63
|
"typecheck": "tsc --noEmit",
|
|
67
64
|
"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
65
|
"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"
|