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 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: `lightpanda` on `PATH`, then `PI_LIGHTPANDA_BIN`, then
176
- `webRender.lightpandaPath`. Prebuilt binaries exist for Linux (glibc; musl needs a source
177
- build) and macOS, plus Docker images; Windows needs WSL2.
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. The script prints the path to use.
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` (`PI_LIGHTPANDA_BIN` fallback) | Path to the Lightpanda binary used for local rendering |
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.0",
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. Point
31
- `webRender.lightpandaPath` (settings.json) or `PI_LIGHTPANDA_BIN` at the printed path.
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"