From c97b7d44e5ec15cf9c9a1834beb5f5ff0d5a6423 Mon Sep 17 00:00:00 2001 From: Stefano Fiorini Date: Thu, 9 Apr 2026 10:21:21 -0500 Subject: [PATCH] feat(web-automation): implement milestone M2 mirror and docs --- README.md | 6 +- docs/WEB-AUTOMATION.md | 99 ++- skills/web-automation/claude-code/SKILL.md | 75 +- .../claude-code/scripts/auth.ts | 18 +- .../claude-code/scripts/browse.ts | 49 +- .../claude-code/scripts/check-install.js | 49 ++ .../claude-code/scripts/extract.js | 188 +++++ .../claude-code/scripts/flow.ts | 329 +++++++++ .../claude-code/scripts/package.json | 31 +- .../claude-code/scripts/pnpm-lock.yaml | 690 +++++------------- .../claude-code/scripts/reference-source.json | 24 + .../claude-code/scripts/scan-local-app.ts | 174 +++++ .../claude-code/scripts/test-full.ts | 17 +- .../claude-code/scripts/test-minimal.ts | 7 +- .../claude-code/scripts/test-profile.ts | 13 +- skills/web-automation/opencode/SKILL.md | 79 +- .../web-automation/opencode/scripts/auth.ts | 18 +- .../web-automation/opencode/scripts/browse.ts | 49 +- .../opencode/scripts/check-install.js | 49 ++ .../opencode/scripts/extract.js | 188 +++++ .../web-automation/opencode/scripts/flow.ts | 329 +++++++++ .../opencode/scripts/package.json | 15 +- .../opencode/scripts/pnpm-lock.yaml | 471 ++---------- .../opencode/scripts/reference-source.json | 24 + .../opencode/scripts/scan-local-app.ts | 284 ++++--- .../opencode/scripts/test-full.ts | 17 +- .../opencode/scripts/test-minimal.ts | 7 +- .../opencode/scripts/test-profile.ts | 13 +- .../tmp-extract-firsthorizon-colors.ts | 78 -- 29 files changed, 2081 insertions(+), 1309 deletions(-) create mode 100644 skills/web-automation/claude-code/scripts/check-install.js create mode 100755 skills/web-automation/claude-code/scripts/extract.js create mode 100644 skills/web-automation/claude-code/scripts/flow.ts create mode 100644 skills/web-automation/claude-code/scripts/reference-source.json create mode 100644 skills/web-automation/claude-code/scripts/scan-local-app.ts create mode 100644 skills/web-automation/opencode/scripts/check-install.js create mode 100755 skills/web-automation/opencode/scripts/extract.js create mode 100644 skills/web-automation/opencode/scripts/flow.ts create mode 100644 skills/web-automation/opencode/scripts/reference-source.json delete mode 100644 skills/web-automation/opencode/scripts/tmp-extract-firsthorizon-colors.ts diff --git a/README.md b/README.md index 52f148f..3822796 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ ai-coding-skills/ | implement-plan | claude-code | Worktree-isolated plan execution with iterative cross-model milestone review | Ready | [IMPLEMENT-PLAN](docs/IMPLEMENT-PLAN.md) | | implement-plan | opencode | Worktree-isolated plan execution with iterative cross-model milestone review | Ready | [IMPLEMENT-PLAN](docs/IMPLEMENT-PLAN.md) | | implement-plan | cursor | Worktree-isolated plan execution with iterative cross-model milestone review | Ready | [IMPLEMENT-PLAN](docs/IMPLEMENT-PLAN.md) | -| web-automation | codex | Playwright + Camoufox browsing/scraping/auth automation | Ready | [WEB-AUTOMATION](docs/WEB-AUTOMATION.md) | -| web-automation | claude-code | Playwright + Camoufox browsing/scraping/auth automation | Ready | [WEB-AUTOMATION](docs/WEB-AUTOMATION.md) | -| web-automation | opencode | Playwright + Camoufox browsing/scraping/auth automation | Ready | [WEB-AUTOMATION](docs/WEB-AUTOMATION.md) | +| web-automation | codex | CloakBrowser-backed browsing, scraping, auth, flow automation, and install validation | Ready | [WEB-AUTOMATION](docs/WEB-AUTOMATION.md) | +| web-automation | claude-code | CloakBrowser-backed browsing, scraping, auth, flow automation, and install validation | Ready | [WEB-AUTOMATION](docs/WEB-AUTOMATION.md) | +| web-automation | opencode | CloakBrowser-backed browsing, scraping, auth, flow automation, and install validation | Ready | [WEB-AUTOMATION](docs/WEB-AUTOMATION.md) | - Docs index: `docs/README.md` - Atlassian guide: `docs/ATLASSIAN.md` diff --git a/docs/WEB-AUTOMATION.md b/docs/WEB-AUTOMATION.md index f07ef9c..c60e281 100644 --- a/docs/WEB-AUTOMATION.md +++ b/docs/WEB-AUTOMATION.md @@ -2,15 +2,25 @@ ## Purpose -Automate browsing and scraping with Playwright + Camoufox. +Automate rendered browsing, scraping, authentication, and multi-step browser flows with Playwright-compatible CloakBrowser. + +## What Ships In Every Variant + +- `browse.ts` for direct navigation and screenshots +- `auth.ts` for form and Microsoft SSO login flows +- `scrape.ts` for markdown extraction +- `flow.ts` for natural-language or JSON browser steps +- `extract.js` for one-shot rendered JSON extraction +- `check-install.js` for install and wiring validation +- `scan-local-app.ts` for configurable local-app smoke scans ## Requirements - Node.js 20+ - pnpm +- `cloakbrowser` - `playwright-core` -- `camoufox-js` -- Network access to download Camoufox browser artifacts +- Network access to download the CloakBrowser binary on first use ## Install @@ -21,8 +31,9 @@ mkdir -p ~/.codex/skills/web-automation cp -R skills/web-automation/codex/* ~/.codex/skills/web-automation/ cd ~/.codex/skills/web-automation/scripts pnpm install -pnpm add playwright-core camoufox-js -npx camoufox-js fetch +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild ``` ### Claude Code @@ -32,8 +43,9 @@ mkdir -p ~/.claude/skills/web-automation cp -R skills/web-automation/claude-code/* ~/.claude/skills/web-automation/ cd ~/.claude/skills/web-automation/scripts pnpm install -pnpm add playwright-core camoufox-js -npx camoufox-js fetch +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild ``` ### OpenCode @@ -43,25 +55,80 @@ mkdir -p ~/.config/opencode/skills/web-automation cp -R skills/web-automation/opencode/* ~/.config/opencode/skills/web-automation/ cd ~/.config/opencode/skills/web-automation/scripts pnpm install -pnpm add playwright-core camoufox-js -npx camoufox-js fetch +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild ``` -## Verify Installation & Dependencies +## Update To The Latest CloakBrowser + +Run inside the installed `scripts/` directory for the variant you are using: + +```bash +pnpm up cloakbrowser playwright-core +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild +``` + +This repo intentionally treats `cloakbrowser` as a refreshable dependency: update to the latest available compatible release, then regenerate the lockfile from that resolved set. + +## Verify Installation & Wiring Run in the installed `scripts/` folder: ```bash -node -e "require.resolve('playwright-core/package.json');require.resolve('camoufox-js/package.json');console.log('OK: playwright-core + camoufox-js installed')" -node -e "const fs=require('fs');const t=fs.readFileSync('browse.ts','utf8');if(!/camoufox-js/.test(t)){throw new Error('browse.ts is not configured for Camoufox')}console.log('OK: Camoufox integration detected in browse.ts')" +node check-install.js ``` -If checks fail, stop and return: +Expected checks: -"Missing dependency/config: web-automation requires `playwright-core` + `camoufox-js` and Camoufox-based scripts. Run setup in this skill, then retry." +- `cloakbrowser` and `playwright-core` resolve correctly +- `browse.ts` is wired to CloakBrowser +- the frozen reference repo + commit recorded in `reference-source.json` are visible to the operator + +If the check fails, stop and return: + +"Missing dependency/config: web-automation requires `cloakbrowser` and `playwright-core` with CloakBrowser-based scripts. Run setup in this skill, then retry." + +If runtime later fails with native-binding issues, run: + +```bash +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild +``` + +## Environment Variables + +- `CLOAKBROWSER_PROFILE_PATH` +- `CLOAKBROWSER_HEADLESS` +- `CLOAKBROWSER_USERNAME` +- `CLOAKBROWSER_PASSWORD` + +There are no `CAMOUFOX_*` compatibility aliases in this migration. ## Usage Examples - Browse: `npx tsx browse.ts --url "https://example.com"` -- Scrape: `npx tsx scrape.ts --url "https://example.com" --mode main --output page.md` -- Auth: `npx tsx auth.ts --url "https://example.com/login"` +- Scrape markdown: `npx tsx scrape.ts --url "https://example.com" --mode main --output page.md` +- Authenticate: `npx tsx auth.ts --url "https://example.com/login"` +- Natural-language flow: `npx tsx flow.ts --instruction 'go to https://example.com then click on "Login"'` +- JSON extract: `node extract.js "https://example.com"` +- Local smoke scan: `SCAN_BASE_URL=http://localhost:3000 SCAN_ROUTES=/,/dashboard npx tsx scan-local-app.ts` + +## Local App Smoke Scan + +`scan-local-app.ts` is generic. Configure it with: + +- `SCAN_BASE_URL` +- `SCAN_LOGIN_PATH` +- `SCAN_USERNAME` +- `SCAN_PASSWORD` +- `SCAN_USERNAME_SELECTOR` +- `SCAN_PASSWORD_SELECTOR` +- `SCAN_SUBMIT_SELECTOR` +- `SCAN_ROUTES` +- `SCAN_REPORT_PATH` +- `SCAN_HEADLESS` + +If `SCAN_USERNAME` or `SCAN_PASSWORD` are omitted, the script falls back to `CLOAKBROWSER_USERNAME` and `CLOAKBROWSER_PASSWORD`. diff --git a/skills/web-automation/claude-code/SKILL.md b/skills/web-automation/claude-code/SKILL.md index 904d984..d375e0f 100644 --- a/skills/web-automation/claude-code/SKILL.md +++ b/skills/web-automation/claude-code/SKILL.md @@ -1,48 +1,101 @@ --- name: web-automation -description: Browse and scrape web pages using Playwright with Camoufox anti-detection browser. Use when automating web workflows, extracting page content to markdown, handling authenticated sessions, or scraping websites with bot protection. +description: Browse and scrape web pages using Playwright-compatible CloakBrowser. Use when automating web workflows, extracting rendered page content, handling authenticated sessions, or running multi-step browser flows. --- -# Web Automation with Camoufox (Claude Code) +# Web Automation with CloakBrowser (Claude Code) -Automated web browsing and scraping using Playwright with Camoufox anti-detection browser. +Automated web browsing and scraping using Playwright-compatible CloakBrowser with two execution paths: + +- one-shot extraction via `extract.js` +- broader stateful automation via `auth.ts`, `browse.ts`, `flow.ts`, `scan-local-app.ts`, and `scrape.ts` ## Requirements - Node.js 20+ - pnpm -- Network access to download browser binaries +- Network access to download the CloakBrowser binary on first use ## First-Time Setup ```bash cd ~/.claude/skills/web-automation/scripts pnpm install -npx camoufox-js fetch +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild +``` + +## Updating CloakBrowser + +```bash +cd ~/.claude/skills/web-automation/scripts +pnpm up cloakbrowser playwright-core +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild ``` ## Prerequisite Check (MANDATORY) -Before running any automation, verify Playwright + Camoufox dependencies are installed and scripts are configured to use Camoufox. +Before running automation, verify CloakBrowser and Playwright Core are installed and wired correctly. ```bash cd ~/.claude/skills/web-automation/scripts -node -e "require.resolve('playwright-core/package.json');require.resolve('camoufox-js/package.json');console.log('OK: playwright-core + camoufox-js installed')" -node -e "const fs=require('fs');const t=fs.readFileSync('browse.ts','utf8');if(!/camoufox-js/.test(t)){throw new Error('browse.ts is not configured for Camoufox')}console.log('OK: Camoufox integration detected in browse.ts')" +node check-install.js ``` -If any check fails, stop and return: +`check-install.js` also prints the frozen reference repo + commit recorded in `reference-source.json`, so operators can confirm the canonical import source before using the skill. -"Missing dependency/config: web-automation requires `playwright-core` + `camoufox-js` and Camoufox-based scripts. Run setup in this skill, then retry." +If the check fails, stop and return: + +"Missing dependency/config: web-automation requires `cloakbrowser` and `playwright-core` with CloakBrowser-based scripts. Run setup in this skill, then retry." + +If runtime fails with missing native bindings for `better-sqlite3` or `esbuild`, run: + +```bash +cd ~/.claude/skills/web-automation/scripts +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild +``` + +## When To Use Which Command + +- Use `node extract.js ""` for a one-shot rendered fetch with JSON output. +- Use `npx tsx scrape.ts ...` when you need markdown extraction, Readability cleanup, or selector-based scraping. +- Use `npx tsx browse.ts ...`, `auth.ts`, or `flow.ts` when the task needs login handling, persistent sessions, clicks, typing, screenshots, or multi-step navigation. +- Use `npx tsx scan-local-app.ts` when you need a configurable local-app smoke pass driven by `SCAN_*` and `CLOAKBROWSER_*` environment variables. ## Quick Reference +- Install check: `node check-install.js` +- One-shot JSON extract: `node extract.js "https://example.com"` - Browse page: `npx tsx browse.ts --url "https://example.com"` - Scrape markdown: `npx tsx scrape.ts --url "https://example.com" --mode main --output page.md` - Authenticate: `npx tsx auth.ts --url "https://example.com/login"` +- Natural-language flow: `npx tsx flow.ts --instruction 'go to https://example.com then click on "Login" then type "user@example.com" in #email then press enter'` +- Local app smoke scan: `SCAN_BASE_URL=http://localhost:3000 SCAN_ROUTES=/,/dashboard npx tsx scan-local-app.ts` + +## Local App Smoke Scan + +`scan-local-app.ts` is intentionally generic. Configure it with environment variables instead of editing the file: + +- `SCAN_BASE_URL` +- `SCAN_LOGIN_PATH` +- `SCAN_USERNAME` +- `SCAN_PASSWORD` +- `SCAN_USERNAME_SELECTOR` +- `SCAN_PASSWORD_SELECTOR` +- `SCAN_SUBMIT_SELECTOR` +- `SCAN_ROUTES` +- `SCAN_REPORT_PATH` +- `SCAN_HEADLESS` + +If `SCAN_USERNAME` or `SCAN_PASSWORD` are omitted, the script falls back to `CLOAKBROWSER_USERNAME` and `CLOAKBROWSER_PASSWORD`. ## Notes -- Sessions persist in Camoufox profile storage. +- Sessions persist in CloakBrowser profile storage. - Use `--wait` for dynamic pages. - Use `--mode selector --selector "..."` for targeted extraction. +- `extract.js` keeps a bounded stealth/rendered fetch path without needing a long-lived automation session. diff --git a/skills/web-automation/claude-code/scripts/auth.ts b/skills/web-automation/claude-code/scripts/auth.ts index 5c25b98..e79f23d 100644 --- a/skills/web-automation/claude-code/scripts/auth.ts +++ b/skills/web-automation/claude-code/scripts/auth.ts @@ -41,8 +41,8 @@ function getCredentials(options?: { username?: string; password?: string; }): { username: string; password: string } | null { - const username = options?.username || process.env.CAMOUFOX_USERNAME; - const password = options?.password || process.env.CAMOUFOX_PASSWORD; + const username = options?.username || process.env.CLOAKBROWSER_USERNAME; + const password = options?.password || process.env.CLOAKBROWSER_PASSWORD; if (!username || !password) { return null; @@ -450,7 +450,7 @@ export async function navigateAuthenticated( if (!credentials) { throw new Error( 'Authentication required but no credentials provided. ' + - 'Set CAMOUFOX_USERNAME and CAMOUFOX_PASSWORD environment variables.' + 'Set CLOAKBROWSER_USERNAME and CLOAKBROWSER_PASSWORD environment variables.' ); } @@ -504,8 +504,8 @@ Usage: Options: -u, --url URL to authenticate (required) -t, --type Auth type: auto, form, or msal (default: auto) - --username Username/email (or set CAMOUFOX_USERNAME env var) - --password Password (or set CAMOUFOX_PASSWORD env var) + --username Username/email (or set CLOAKBROWSER_USERNAME env var) + --password Password (or set CLOAKBROWSER_PASSWORD env var) --headless Run in headless mode (default: false for auth) -h, --help Show this help message @@ -515,8 +515,8 @@ Auth Types: msal Microsoft SSO (login.microsoftonline.com) Environment Variables: - CAMOUFOX_USERNAME Default username/email for authentication - CAMOUFOX_PASSWORD Default password for authentication + CLOAKBROWSER_USERNAME Default username/email for authentication + CLOAKBROWSER_PASSWORD Default password for authentication Examples: # Interactive login (no credentials, opens browser) @@ -527,11 +527,11 @@ Examples: --username "user@example.com" --password "secret" # Microsoft SSO login - CAMOUFOX_USERNAME=user@company.com CAMOUFOX_PASSWORD=secret \\ + CLOAKBROWSER_USERNAME=user@company.com CLOAKBROWSER_PASSWORD=secret \\ npx tsx auth.ts --url "https://internal.company.com" --type msal Notes: - - Session is saved to ~/.camoufox-profile/ for persistence + - Session is saved to ~/.cloakbrowser-profile/ for persistence - After successful auth, subsequent browses will be authenticated - Use --headless false if you need to handle MFA manually `); diff --git a/skills/web-automation/claude-code/scripts/browse.ts b/skills/web-automation/claude-code/scripts/browse.ts index 901089b..01cf098 100644 --- a/skills/web-automation/claude-code/scripts/browse.ts +++ b/skills/web-automation/claude-code/scripts/browse.ts @@ -1,7 +1,7 @@ #!/usr/bin/env npx tsx /** - * Browser launcher using Camoufox with persistent profile + * Browser launcher using CloakBrowser with persistent profile * * Usage: * npx tsx browse.ts --url "https://example.com" @@ -9,14 +9,13 @@ * npx tsx browse.ts --url "https://example.com" --headless false --wait 5000 */ -import { Camoufox } from 'camoufox-js'; +import { launchPersistentContext } from 'cloakbrowser'; import { homedir } from 'os'; import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; import parseArgs from 'minimist'; import type { Page, BrowserContext } from 'playwright-core'; -// Types interface BrowseOptions { url: string; headless?: boolean; @@ -33,55 +32,54 @@ interface BrowseResult { screenshotPath?: string; } -// Get profile directory +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + const getProfilePath = (): string => { - const customPath = process.env.CAMOUFOX_PROFILE_PATH; + const customPath = process.env.CLOAKBROWSER_PROFILE_PATH; if (customPath) return customPath; - const profileDir = join(homedir(), '.camoufox-profile'); + const profileDir = join(homedir(), '.cloakbrowser-profile'); if (!existsSync(profileDir)) { mkdirSync(profileDir, { recursive: true }); } return profileDir; }; -// Launch browser with persistent profile export async function launchBrowser(options: { headless?: boolean; }): Promise { const profilePath = getProfilePath(); - const headless = - options.headless ?? - (process.env.CAMOUFOX_HEADLESS ? process.env.CAMOUFOX_HEADLESS === 'true' : true); + const envHeadless = process.env.CLOAKBROWSER_HEADLESS; + const headless = options.headless ?? (envHeadless ? envHeadless === 'true' : true); console.log(`Using profile: ${profilePath}`); console.log(`Headless mode: ${headless}`); - const browser = await Camoufox({ - user_data_dir: profilePath, + const context = await launchPersistentContext({ + userDataDir: profilePath, headless, + humanize: true, }); - return browser; + return context; } -// Browse to URL and optionally take screenshot export async function browse(options: BrowseOptions): Promise { const browser = await launchBrowser({ headless: options.headless }); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); try { - // Navigate to URL console.log(`Navigating to: ${options.url}`); await page.goto(options.url, { timeout: options.timeout ?? 60000, waitUntil: 'domcontentloaded', }); - // Wait if specified if (options.wait) { console.log(`Waiting ${options.wait}ms...`); - await page.waitForTimeout(options.wait); + await sleep(options.wait); } const result: BrowseResult = { @@ -92,7 +90,6 @@ export async function browse(options: BrowseOptions): Promise { console.log(`Page title: ${result.title}`); console.log(`Final URL: ${result.url}`); - // Take screenshot if requested if (options.screenshot) { const outputPath = options.output ?? 'screenshot.png'; await page.screenshot({ path: outputPath, fullPage: true }); @@ -100,11 +97,10 @@ export async function browse(options: BrowseOptions): Promise { console.log(`Screenshot saved: ${outputPath}`); } - // If interactive mode, keep browser open if (options.interactive) { console.log('\nInteractive mode - browser will stay open.'); console.log('Press Ctrl+C to close.'); - await new Promise(() => {}); // Keep running + await new Promise(() => {}); } return result; @@ -115,16 +111,14 @@ export async function browse(options: BrowseOptions): Promise { } } -// Export page for use in other scripts export async function getPage(options?: { headless?: boolean; }): Promise<{ page: Page; browser: BrowserContext }> { const browser = await launchBrowser({ headless: options?.headless }); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); return { page, browser }; } -// CLI entry point async function main() { const args = parseArgs(process.argv.slice(2), { string: ['url', 'output'], @@ -145,7 +139,7 @@ async function main() { if (args.help || !args.url) { console.log(` -Web Browser with Camoufox +Web Browser with CloakBrowser Usage: npx tsx browse.ts --url [options] @@ -166,8 +160,8 @@ Examples: npx tsx browse.ts --url "https://example.com" --headless false --interactive Environment Variables: - CAMOUFOX_PROFILE_PATH Custom profile directory (default: ~/.camoufox-profile/) - CAMOUFOX_HEADLESS Default headless mode (true/false) + CLOAKBROWSER_PROFILE_PATH Custom profile directory (default: ~/.cloakbrowser-profile/) + CLOAKBROWSER_HEADLESS Default headless mode (true/false) `); process.exit(args.help ? 0 : 1); } @@ -188,7 +182,6 @@ Environment Variables: } } -// Run if executed directly const isMainModule = process.argv[1]?.includes('browse.ts'); if (isMainModule) { main(); diff --git a/skills/web-automation/claude-code/scripts/check-install.js b/skills/web-automation/claude-code/scripts/check-install.js new file mode 100644 index 0000000..9423779 --- /dev/null +++ b/skills/web-automation/claude-code/scripts/check-install.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const referencePath = path.join(__dirname, "reference-source.json"); + +function fail(message, details) { + const payload = { error: message }; + if (details) payload.details = details; + process.stderr.write(`${JSON.stringify(payload)}\n`); + process.exit(1); +} + +async function main() { + try { + await import("cloakbrowser"); + await import("playwright-core"); + } catch (error) { + fail( + "Missing dependency/config: web-automation requires cloakbrowser and playwright-core.", + error instanceof Error ? error.message : String(error) + ); + } + + const browsePath = path.join(__dirname, "browse.ts"); + const browseSource = fs.readFileSync(browsePath, "utf8"); + if (!/launchPersistentContext/.test(browseSource) || !/from ['"]cloakbrowser['"]/.test(browseSource)) { + fail("browse.ts is not configured for CloakBrowser."); + } + + const referenceSource = JSON.parse(fs.readFileSync(referencePath, "utf8")); + if (!referenceSource.referenceRepo || !referenceSource.referenceCommit) { + fail("Frozen reference metadata is missing from reference-source.json."); + } + + process.stdout.write("OK: cloakbrowser + playwright-core installed\n"); + process.stdout.write("OK: CloakBrowser integration detected in browse.ts\n"); + process.stdout.write( + `OK: frozen reference ${referenceSource.referenceRepo}@${referenceSource.referenceCommit}\n` + ); +} + +main().catch((error) => { + fail("Install check failed.", error instanceof Error ? error.message : String(error)); +}); diff --git a/skills/web-automation/claude-code/scripts/extract.js b/skills/web-automation/claude-code/scripts/extract.js new file mode 100755 index 0000000..5e3908a --- /dev/null +++ b/skills/web-automation/claude-code/scripts/extract.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const DEFAULT_WAIT_MS = 5000; +const MAX_WAIT_MS = 20000; +const NAV_TIMEOUT_MS = 30000; +const EXTRA_CHALLENGE_WAIT_MS = 8000; +const CONTENT_LIMIT = 12000; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function fail(message, details) { + const payload = { error: message }; + if (details) payload.details = details; + process.stderr.write(`${JSON.stringify(payload)}\n`); + process.exit(1); +} + +function parseWaitTime(raw) { + const value = Number.parseInt(raw || `${DEFAULT_WAIT_MS}`, 10); + if (!Number.isFinite(value) || value < 0) return DEFAULT_WAIT_MS; + return Math.min(value, MAX_WAIT_MS); +} + +function parseTarget(rawUrl) { + if (!rawUrl) { + fail("Missing URL. Usage: node extract.js "); + } + + let parsed; + try { + parsed = new URL(rawUrl); + } catch (error) { + fail("Invalid URL.", error.message); + } + + if (!["http:", "https:"].includes(parsed.protocol)) { + fail("Only http and https URLs are allowed."); + } + + return parsed.toString(); +} + +function ensureParentDir(filePath) { + if (!filePath) return; + fs.mkdirSync(path.dirname(filePath), { recursive: true }); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function detectChallenge(page) { + try { + return await page.evaluate(() => { + const text = (document.body?.innerText || "").toLowerCase(); + return ( + text.includes("checking your browser") || + text.includes("just a moment") || + text.includes("verify you are human") || + text.includes("press and hold") || + document.querySelector('iframe[src*="challenge"]') !== null || + document.querySelector('iframe[src*="cloudflare"]') !== null + ); + }); + } catch { + return false; + } +} + +async function loadCloakBrowser() { + try { + return await import("cloakbrowser"); + } catch (error) { + fail( + "CloakBrowser is not installed for this skill. Run pnpm install in this skill's scripts directory first.", + error.message + ); + } +} + +async function runWithStderrLogs(fn) { + const originalLog = console.log; + const originalError = console.error; + console.log = (...args) => process.stderr.write(`${args.join(" ")}\n`); + console.error = (...args) => process.stderr.write(`${args.join(" ")}\n`); + try { + return await fn(); + } finally { + console.log = originalLog; + console.error = originalError; + } +} + +async function main() { + const requestedUrl = parseTarget(process.argv[2]); + const waitTime = parseWaitTime(process.env.WAIT_TIME); + const screenshotPath = process.env.SCREENSHOT_PATH || ""; + const saveHtml = process.env.SAVE_HTML === "true"; + const headless = process.env.HEADLESS !== "false"; + const userAgent = process.env.USER_AGENT || undefined; + const startedAt = Date.now(); + const { ensureBinary, launchContext } = await loadCloakBrowser(); + + let context; + try { + await runWithStderrLogs(() => ensureBinary()); + + context = await runWithStderrLogs(() => launchContext({ + headless, + userAgent, + locale: "en-US", + viewport: { width: 1440, height: 900 }, + humanize: true, + })); + + const page = await context.newPage(); + const response = await page.goto(requestedUrl, { + waitUntil: "domcontentloaded", + timeout: NAV_TIMEOUT_MS + }); + + await sleep(waitTime); + + let challengeDetected = await detectChallenge(page); + if (challengeDetected) { + await sleep(EXTRA_CHALLENGE_WAIT_MS); + challengeDetected = await detectChallenge(page); + } + + const extracted = await page.evaluate((contentLimit) => { + const bodyText = document.body?.innerText || ""; + return { + finalUrl: window.location.href, + title: document.title || "", + content: bodyText.slice(0, contentLimit), + metaDescription: + document.querySelector('meta[name="description"]')?.content || + document.querySelector('meta[property="og:description"]')?.content || + "" + }; + }, CONTENT_LIMIT); + + const result = { + requestedUrl, + finalUrl: extracted.finalUrl, + title: extracted.title, + content: extracted.content, + metaDescription: extracted.metaDescription, + status: response ? response.status() : null, + challengeDetected, + elapsedSeconds: ((Date.now() - startedAt) / 1000).toFixed(2) + }; + + if (screenshotPath) { + ensureParentDir(screenshotPath); + await page.screenshot({ path: screenshotPath, fullPage: false, timeout: 10000 }); + result.screenshot = screenshotPath; + } + + if (saveHtml) { + const htmlTarget = screenshotPath + ? screenshotPath.replace(/\.[^.]+$/, ".html") + : path.resolve(__dirname, `page-${Date.now()}.html`); + ensureParentDir(htmlTarget); + fs.writeFileSync(htmlTarget, await page.content()); + result.htmlFile = htmlTarget; + } + + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + await context.close(); + } catch (error) { + if (context) { + try { + await context.close(); + } catch { + // Ignore close errors after the primary failure. + } + } + fail("Scrape failed.", error.message); + } +} + +main(); diff --git a/skills/web-automation/claude-code/scripts/flow.ts b/skills/web-automation/claude-code/scripts/flow.ts new file mode 100644 index 0000000..5d01e55 --- /dev/null +++ b/skills/web-automation/claude-code/scripts/flow.ts @@ -0,0 +1,329 @@ +#!/usr/bin/env npx tsx + +import parseArgs from 'minimist'; +import type { Page } from 'playwright-core'; +import { launchBrowser } from './browse'; + +type Step = + | { action: 'goto'; url: string } + | { action: 'click'; selector?: string; text?: string; role?: string; name?: string } + | { action: 'type'; selector?: string; text: string } + | { action: 'press'; key: string; selector?: string } + | { action: 'wait'; ms: number } + | { action: 'screenshot'; path: string } + | { action: 'extract'; selector: string; count?: number }; + +function normalizeNavigationUrl(rawUrl: string): string { + let parsed: URL; + try { + parsed = new URL(rawUrl); + } catch { + throw new Error(`Invalid navigation URL: ${rawUrl}`); + } + + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error(`Only http and https URLs are allowed in flow steps: ${rawUrl}`); + } + + return parsed.toString(); +} + +function normalizeKey(k: string): string { + if (!k) return 'Enter'; + const lower = k.toLowerCase(); + if (lower === 'enter' || lower === 'return') return 'Enter'; + if (lower === 'tab') return 'Tab'; + if (lower === 'escape' || lower === 'esc') return 'Escape'; + return k; +} + +function splitInstructions(instruction: string): string[] { + return instruction + .split(/\bthen\b|;/gi) + .map((s) => s.trim()) + .filter(Boolean); +} + +function parseInstruction(instruction: string): Step[] { + const parts = splitInstructions(instruction); + const steps: Step[] = []; + + for (const p of parts) { + // go to https://... + const goto = p.match(/^(?:go to|open|navigate to)\s+(https?:\/\/\S+)/i); + if (goto) { + steps.push({ action: 'goto', url: normalizeNavigationUrl(goto[1]) }); + continue; + } + + // click on "text" or click #selector or click button "name" + const clickRole = p.match(/^click\s+(button|link|textbox|img|image|tab)\s+"([^"]+)"$/i); + if (clickRole) { + const role = clickRole[1].toLowerCase() === 'image' ? 'img' : clickRole[1].toLowerCase(); + steps.push({ action: 'click', role, name: clickRole[2] }); + continue; + } + const clickText = p.match(/^click(?: on)?\s+"([^"]+)"/i); + if (clickText) { + steps.push({ action: 'click', text: clickText[1] }); + continue; + } + const clickSelector = p.match(/^click(?: on)?\s+(#[\w-]+|\.[\w-]+|[a-z]+\[[^\]]+\])/i); + if (clickSelector) { + steps.push({ action: 'click', selector: clickSelector[1] }); + continue; + } + + // type "text" [in selector] + const typeInto = p.match(/^type\s+"([^"]+)"\s+in\s+(.+)$/i); + if (typeInto) { + steps.push({ action: 'type', text: typeInto[1], selector: typeInto[2].trim() }); + continue; + } + const typeOnly = p.match(/^type\s+"([^"]+)"$/i); + if (typeOnly) { + steps.push({ action: 'type', text: typeOnly[1] }); + continue; + } + + // press enter [in selector] + const pressIn = p.match(/^press\s+(\w+)\s+in\s+(.+)$/i); + if (pressIn) { + steps.push({ action: 'press', key: normalizeKey(pressIn[1]), selector: pressIn[2].trim() }); + continue; + } + const pressOnly = p.match(/^press\s+(\w+)$/i); + if (pressOnly) { + steps.push({ action: 'press', key: normalizeKey(pressOnly[1]) }); + continue; + } + + // wait 2s / wait 500ms + const waitS = p.match(/^wait\s+(\d+)\s*s(?:ec(?:onds?)?)?$/i); + if (waitS) { + steps.push({ action: 'wait', ms: parseInt(waitS[1], 10) * 1000 }); + continue; + } + const waitMs = p.match(/^wait\s+(\d+)\s*ms$/i); + if (waitMs) { + steps.push({ action: 'wait', ms: parseInt(waitMs[1], 10) }); + continue; + } + + // screenshot path + const shot = p.match(/^screenshot(?: to)?\s+(.+)$/i); + if (shot) { + steps.push({ action: 'screenshot', path: shot[1].trim() }); + continue; + } + + throw new Error(`Could not parse step: "${p}"`); + } + + return steps; +} + +function validateSteps(steps: Step[]): Step[] { + return steps.map((step) => + step.action === 'goto' + ? { + ...step, + url: normalizeNavigationUrl(step.url), + } + : step + ); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function isLikelyLoginText(text: string): boolean { + return /(login|accedi|sign\s*in|entra)/i.test(text); +} + +async function clickByText(page: Page, text: string): Promise { + const patterns = [new RegExp(`^${escapeRegExp(text)}$`, 'i'), new RegExp(escapeRegExp(text), 'i')]; + + for (const pattern of patterns) { + const targets = [ + page.getByRole('button', { name: pattern }).first(), + page.getByRole('link', { name: pattern }).first(), + page.getByText(pattern).first(), + ]; + + for (const target of targets) { + if (await target.count()) { + try { + await target.click({ timeout: 8000 }); + return true; + } catch { + // keep trying next candidate + } + } + } + } + + return false; +} + +async function fallbackLoginNavigation(page: Page, requestedText: string): Promise { + if (!isLikelyLoginText(requestedText)) return false; + + const current = new URL(page.url()); + + const candidateLinks = await page.evaluate(() => { + const loginTerms = ['login', 'accedi', 'sign in', 'entra']; + const anchors = Array.from(document.querySelectorAll('a[href], a[onclick], button[onclick]')) as Array; + + return anchors + .map((el) => { + const text = (el.textContent || '').trim().toLowerCase(); + const href = (el as HTMLAnchorElement).getAttribute('href') || ''; + return { text, href }; + }) + .filter((x) => x.text && loginTerms.some((t) => x.text.includes(t))) + .map((x) => x.href) + .filter(Boolean); + }); + + // Prefer real URLs (not javascript:) + const realCandidate = candidateLinks.find((h) => /login|account\/login/i.test(h) && !h.startsWith('javascript:')); + if (realCandidate) { + const target = new URL(realCandidate, page.url()).toString(); + await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 }); + return true; + } + + // Site-specific fallback for Corriere + if (/corriere\.it$/i.test(current.hostname) || /\.corriere\.it$/i.test(current.hostname)) { + await page.goto('https://www.corriere.it/account/login', { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + return true; + } + + return false; +} + +async function typeInBestTarget(page: Page, text: string, selector?: string) { + if (selector) { + await page.locator(selector).first().click({ timeout: 10000 }); + await page.locator(selector).first().fill(text); + return; + } + const loc = page.locator('input[name="q"], input[type="search"], input[type="text"], textarea').first(); + await loc.click({ timeout: 10000 }); + await loc.fill(text); +} + +async function pressOnTarget(page: Page, key: string, selector?: string) { + if (selector) { + await page.locator(selector).first().press(key); + return; + } + await page.keyboard.press(key); +} + +async function runSteps(page: Page, steps: Step[]) { + for (const step of steps) { + switch (step.action) { + case 'goto': + await page.goto(normalizeNavigationUrl(step.url), { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + break; + case 'click': + if (step.selector) { + await page.locator(step.selector).first().click({ timeout: 15000 }); + } else if (step.role && step.name) { + await page.getByRole(step.role as any, { name: new RegExp(escapeRegExp(step.name), 'i') }).first().click({ timeout: 15000 }); + } else if (step.text) { + const clicked = await clickByText(page, step.text); + if (!clicked) { + const recovered = await fallbackLoginNavigation(page, step.text); + if (!recovered) { + throw new Error(`Could not click target text: ${step.text}`); + } + } + } else { + throw new Error('click step missing selector/text/role'); + } + try { + await page.waitForLoadState('domcontentloaded', { timeout: 10000 }); + } catch { + // no navigation is fine + } + break; + case 'type': + await typeInBestTarget(page, step.text, step.selector); + break; + case 'press': + await pressOnTarget(page, step.key, step.selector); + break; + case 'wait': + await page.waitForTimeout(step.ms); + break; + case 'screenshot': + await page.screenshot({ path: step.path, fullPage: true }); + break; + case 'extract': { + const items = await page.locator(step.selector).allTextContents(); + const out = items.slice(0, step.count ?? items.length).map((t) => t.trim()).filter(Boolean); + console.log(JSON.stringify(out, null, 2)); + break; + } + default: + throw new Error('Unknown step'); + } + } +} + +async function main() { + const args = parseArgs(process.argv.slice(2), { + string: ['instruction', 'steps'], + boolean: ['headless', 'help'], + default: { headless: true }, + alias: { i: 'instruction', s: 'steps', h: 'help' }, + }); + + if (args.help || (!args.instruction && !args.steps)) { + console.log(` +General Web Flow Runner (CloakBrowser) + +Usage: + npx tsx flow.ts --instruction "go to https://example.com then type \"hello\" then press enter" + npx tsx flow.ts --steps '[{"action":"goto","url":"https://example.com"}]' + +Supported natural steps: + - go to/open/navigate to + - click on "Text" + - click + - type "text" + - type "text" in + - press + - press in + - wait s | wait ms + - screenshot +`); + process.exit(args.help ? 0 : 1); + } + + const steps = validateSteps(args.steps ? JSON.parse(args.steps) : parseInstruction(args.instruction)); + const browser = await launchBrowser({ headless: args.headless }); + const page = await browser.newPage(); + + try { + await runSteps(page, steps); + console.log('Flow complete. Final URL:', page.url()); + } finally { + await browser.close(); + } +} + +main().catch((e) => { + console.error('Error:', e instanceof Error ? e.message : e); + process.exit(1); +}); diff --git a/skills/web-automation/claude-code/scripts/package.json b/skills/web-automation/claude-code/scripts/package.json index cf729f1..a2221e8 100644 --- a/skills/web-automation/claude-code/scripts/package.json +++ b/skills/web-automation/claude-code/scripts/package.json @@ -1,27 +1,36 @@ { "name": "web-automation-scripts", "version": "1.0.0", - "description": "Web browsing and scraping scripts using Camoufox", + "description": "Web browsing and scraping scripts using CloakBrowser", "type": "module", "scripts": { + "check-install": "node check-install.js", + "extract": "node extract.js", "browse": "tsx browse.ts", + "auth": "tsx auth.ts", + "flow": "tsx flow.ts", "scrape": "tsx scrape.ts", - "fetch-browser": "npx camoufox-js fetch" + "typecheck": "tsc --noEmit -p tsconfig.json", + "lint": "pnpm run typecheck && node --check check-install.js && node --check extract.js", + "fetch-browser": "npx cloakbrowser install" }, "dependencies": { - "camoufox-js": "^0.8.5", - "playwright-core": "^1.40.0", - "turndown": "^7.1.2", - "turndown-plugin-gfm": "^1.0.2", "@mozilla/readability": "^0.5.0", + "better-sqlite3": "^12.6.2", + "cloakbrowser": "^0.3.22", "jsdom": "^24.0.0", - "minimist": "^1.2.8" + "minimist": "^1.2.8", + "playwright-core": "^1.59.1", + "turndown": "^7.1.2", + "turndown-plugin-gfm": "^1.0.2" }, "devDependencies": { - "typescript": "^5.3.0", - "@types/turndown": "^5.0.4", "@types/jsdom": "^21.1.6", "@types/minimist": "^1.2.5", - "tsx": "^4.7.0" - } + "@types/turndown": "^5.0.4", + "esbuild": "0.27.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34" } diff --git a/skills/web-automation/claude-code/scripts/pnpm-lock.yaml b/skills/web-automation/claude-code/scripts/pnpm-lock.yaml index 5fcd57f..59dba9c 100644 --- a/skills/web-automation/claude-code/scripts/pnpm-lock.yaml +++ b/skills/web-automation/claude-code/scripts/pnpm-lock.yaml @@ -11,9 +11,12 @@ importers: '@mozilla/readability': specifier: ^0.5.0 version: 0.5.0 - camoufox-js: - specifier: ^0.8.5 - version: 0.8.5(playwright-core@1.57.0) + better-sqlite3: + specifier: ^12.6.2 + version: 12.8.0 + cloakbrowser: + specifier: ^0.3.22 + version: 0.3.22(mmdb-lib@3.0.1)(playwright-core@1.59.1) jsdom: specifier: ^24.0.0 version: 24.1.3 @@ -21,8 +24,8 @@ importers: specifier: ^1.2.8 version: 1.2.8 playwright-core: - specifier: ^1.40.0 - version: 1.57.0 + specifier: ^1.59.1 + version: 1.59.1 turndown: specifier: ^7.1.2 version: 7.2.2 @@ -39,6 +42,9 @@ importers: '@types/turndown': specifier: ^5.0.4 version: 5.0.6 + esbuild: + specifier: 0.27.0 + version: 0.27.0 tsx: specifier: ^4.7.0 version: 4.21.0 @@ -79,169 +85,165 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -250,10 +252,6 @@ packages: resolution: {integrity: sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==} engines: {node: '>=14.0.0'} - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - '@types/jsdom@21.1.7': resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} @@ -269,10 +267,6 @@ packages: '@types/turndown@5.0.6': resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} - adm-zip@0.5.16: - resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} - engines: {node: '>=12.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -283,12 +277,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.14: - resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} - hasBin: true - - better-sqlite3@12.6.0: - resolution: {integrity: sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ==} + better-sqlite3@12.8.0: + resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} bindings@1.5.0: @@ -297,11 +287,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -309,31 +294,33 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camoufox-js@0.8.5: - resolution: {integrity: sha512-20ihPbspAcOVSUTX9Drxxp0C116DON1n8OVA1eUDglWZiHwiHwFVFOMrIEBwAHMZpU11mIEH/kawJtstRIrDPA==} - engines: {node: '>= 20'} - hasBin: true - peerDependencies: - playwright-core: '*' - - caniuse-lite@1.0.30001764: - resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} - chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cloakbrowser@0.3.22: + resolution: {integrity: sha512-L2CWQiVdunhKslTli8HCe4INhaAt4npbvsM2Ox4/idqiRmT2BADndQ05eDS8TonNSWeWqbjsh04UhSZOD3B6mg==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + mmdb-lib: '>=2.0.0' + playwright-core: '>=1.40.0' + puppeteer-core: '>=21.0.0' + peerDependenciesMeta: + mmdb-lib: + optional: true + playwright-core: + optional: true + puppeteer-core: + optional: true + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} - engines: {node: '>=20'} - cssstyle@4.6.0: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} @@ -366,24 +353,14 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - detect-europe-js@0.1.2: - resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dot-prop@6.0.1: - resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} - engines: {node: '>=10'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -407,15 +384,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -423,10 +396,6 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fingerprint-generator@2.1.79: - resolution: {integrity: sha512-0dr3kTgvRYHleRPp6OBDcPb8amJmOyFr9aOuwnpN6ooWJ5XyT+/aL/SZ6CU4ZrEtzV26EyJ2Lg7PT32a0NdrRA==} - engines: {node: '>=16.0.0'} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -442,9 +411,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - generative-bayesian-network@2.1.79: - resolution: {integrity: sha512-aPH+V2wO+HE0BUX1LbsM8Ak99gmV43lgh+D7GDteM0zgnPqiAwcK9JZPxMPZa3aJUleFtFaL1lAei8g9zNrDIA==} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -459,10 +425,6 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -479,10 +441,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - header-generator@2.1.79: - resolution: {integrity: sha512-YvHx8teq4QmV5mz7wdPMsj9n1OZBPnZxA4QE+EOrtx7xbmGvd1gBvDNKCb5XqS4GR/TL75MU5hqMqqqANdILRg==} - engines: {node: '>=16.0.0'} - html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -502,74 +460,15 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - impit-darwin-arm64@0.7.6: - resolution: {integrity: sha512-M7NQXkttyzqilWfzVkNCp7hApT69m0etyJkVpHze4bR5z1kJnHhdsb8BSdDv2dzvZL4u1JyqZNxq+qoMn84eUw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - impit-darwin-x64@0.7.6: - resolution: {integrity: sha512-kikTesWirAwJp9JPxzGLoGVc+heBlEabWS5AhTkQedACU153vmuL90OBQikVr3ul2N0LPImvnuB+51wV0zDE6g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - impit-linux-arm64-gnu@0.7.6: - resolution: {integrity: sha512-H6GHjVr/0lG9VEJr6IHF8YLq+YkSIOF4k7Dfue2ygzUAj1+jZ5ZwnouhG/XrZHYW6EWsZmEAjjRfWE56Q0wDRQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - impit-linux-arm64-musl@0.7.6: - resolution: {integrity: sha512-1sCB/UBVXLZTpGJsXRdNNSvhN9xmmQcYLMWAAB4Itb7w684RHX1pLoCb6ichv7bfAf6tgaupcFIFZNBp3ghmQA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - impit-linux-x64-gnu@0.7.6: - resolution: {integrity: sha512-yYhlRnZ4fhKt8kuGe0JK2WSHc8TkR6BEH0wn+guevmu8EOn9Xu43OuRvkeOyVAkRqvFnlZtMyySUo/GuSLz9Gw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - impit-linux-x64-musl@0.7.6: - resolution: {integrity: sha512-sdGWyu+PCLmaOXy7Mzo4WP61ZLl5qpZ1L+VeXW+Ycazgu0e7ox0NZLdiLRunIrEzD+h0S+e4CyzNwaiP3yIolg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - impit-win32-arm64-msvc@0.7.6: - resolution: {integrity: sha512-sM5deBqo0EuXg5GACBUMKEua9jIau/i34bwNlfrf/Amnw1n0GB4/RkuUh+sKiUcbNAntrRq+YhCq8qDP8IW19w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - impit-win32-x64-msvc@0.7.6: - resolution: {integrity: sha512-ry63ADGLCB/PU/vNB1VioRt2V+klDJ34frJUXUZBEv1kA96HEAg9AxUk+604o+UHS3ttGH2rkLmrbwHOdAct5Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - impit@0.7.6: - resolution: {integrity: sha512-AkS6Gv63+E6GMvBrcRhMmOREKpq5oJ0J5m3xwfkHiEs97UIsbpEqFmW3sFw/sdyOTDGRF5q4EjaLxtb922Ta8g==} - engines: {node: '>= 20'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-standalone-pwa@0.1.1: - resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} - jsdom@24.1.3: resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} engines: {node: '>=18'} @@ -579,32 +478,13 @@ packages: canvas: optional: true - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@2.1.0: - resolution: {integrity: sha512-D4CgpyCt+61f6z2jHjJS1OmZPviAWM57iJ9OKdFFWSNgS7Udj9QVWqyGs/cveVNF57XpZmhSvMdVIV5mjLA7Vg==} - engines: {node: '>=22'} - - lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - maxmind@5.0.3: - resolution: {integrity: sha512-oMtZwLrsp0LcZehfYKIirtwKMBycMMqMA1/Dc9/BlUqIEtXO75mIzMJ3PYCV1Ji+BpoUCk+lTzRfh9c+ptGdyQ==} - engines: {node: '>=12', npm: '>=6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -617,10 +497,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -628,6 +504,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -645,43 +525,26 @@ packages: resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} engines: {node: '>=10'} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - ow@0.28.2: - resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} - engines: {node: '>=12'} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -721,10 +584,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.4.4: - resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} - engines: {node: '>=11.0.0'} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -757,9 +616,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tiny-lru@11.4.5: - resolution: {integrity: sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==} - engines: {node: '>=12'} + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} + engines: {node: '>=18'} tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} @@ -769,9 +628,6 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -791,13 +647,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-is-frozen@0.1.2: - resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - - ua-parser-js@2.0.7: - resolution: {integrity: sha512-CFdHVHr+6YfbktNZegH3qbYvYgC7nRNEUm2tk7nSFXSODUu4tDBpaFpP1jdXBUOKKwapVlWRfTtS8bCPzsQ47w==} - hasBin: true - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -805,22 +654,12 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vali-date@1.0.0: - resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} - engines: {node: '>=0.10.0'} - w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -861,17 +700,13 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml2js@0.6.2: - resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + snapshots: '@asamuzakjp/css-color@3.2.0': @@ -902,96 +737,92 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@esbuild/aix-ppc64@0.27.2': + '@esbuild/aix-ppc64@0.27.0': optional: true - '@esbuild/android-arm64@0.27.2': + '@esbuild/android-arm64@0.27.0': optional: true - '@esbuild/android-arm@0.27.2': + '@esbuild/android-arm@0.27.0': optional: true - '@esbuild/android-x64@0.27.2': + '@esbuild/android-x64@0.27.0': optional: true - '@esbuild/darwin-arm64@0.27.2': + '@esbuild/darwin-arm64@0.27.0': optional: true - '@esbuild/darwin-x64@0.27.2': + '@esbuild/darwin-x64@0.27.0': optional: true - '@esbuild/freebsd-arm64@0.27.2': + '@esbuild/freebsd-arm64@0.27.0': optional: true - '@esbuild/freebsd-x64@0.27.2': + '@esbuild/freebsd-x64@0.27.0': optional: true - '@esbuild/linux-arm64@0.27.2': + '@esbuild/linux-arm64@0.27.0': optional: true - '@esbuild/linux-arm@0.27.2': + '@esbuild/linux-arm@0.27.0': optional: true - '@esbuild/linux-ia32@0.27.2': + '@esbuild/linux-ia32@0.27.0': optional: true - '@esbuild/linux-loong64@0.27.2': + '@esbuild/linux-loong64@0.27.0': optional: true - '@esbuild/linux-mips64el@0.27.2': + '@esbuild/linux-mips64el@0.27.0': optional: true - '@esbuild/linux-ppc64@0.27.2': + '@esbuild/linux-ppc64@0.27.0': optional: true - '@esbuild/linux-riscv64@0.27.2': + '@esbuild/linux-riscv64@0.27.0': optional: true - '@esbuild/linux-s390x@0.27.2': + '@esbuild/linux-s390x@0.27.0': optional: true - '@esbuild/linux-x64@0.27.2': + '@esbuild/linux-x64@0.27.0': optional: true - '@esbuild/netbsd-arm64@0.27.2': + '@esbuild/netbsd-arm64@0.27.0': optional: true - '@esbuild/netbsd-x64@0.27.2': + '@esbuild/netbsd-x64@0.27.0': optional: true - '@esbuild/openbsd-arm64@0.27.2': + '@esbuild/openbsd-arm64@0.27.0': optional: true - '@esbuild/openbsd-x64@0.27.2': + '@esbuild/openbsd-x64@0.27.0': optional: true - '@esbuild/openharmony-arm64@0.27.2': + '@esbuild/openharmony-arm64@0.27.0': optional: true - '@esbuild/sunos-x64@0.27.2': + '@esbuild/sunos-x64@0.27.0': optional: true - '@esbuild/win32-arm64@0.27.2': + '@esbuild/win32-arm64@0.27.0': optional: true - '@esbuild/win32-ia32@0.27.2': + '@esbuild/win32-ia32@0.27.0': optional: true - '@esbuild/win32-x64@0.27.2': + '@esbuild/win32-x64@0.27.0': optional: true - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': + '@isaacs/fs-minipass@4.0.1': dependencies: - '@isaacs/balanced-match': 4.0.1 + minipass: 7.1.2 '@mixmark-io/domino@2.2.0': {} '@mozilla/readability@0.5.0': {} - '@sindresorhus/is@4.6.0': {} - '@types/jsdom@21.1.7': dependencies: '@types/node': 25.0.6 @@ -1008,17 +839,13 @@ snapshots: '@types/turndown@5.0.6': {} - adm-zip@0.5.16: {} - agent-base@7.1.4: {} asynckit@0.4.0: {} base64-js@1.5.1: {} - baseline-browser-mapping@2.9.14: {} - - better-sqlite3@12.6.0: + better-sqlite3@12.8.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -1033,14 +860,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001764 - electron-to-chromium: 1.5.267 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -1051,33 +870,21 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - callsites@3.1.0: {} - - camoufox-js@0.8.5(playwright-core@1.57.0): - dependencies: - adm-zip: 0.5.16 - better-sqlite3: 12.6.0 - commander: 14.0.2 - fingerprint-generator: 2.1.79 - glob: 13.0.0 - impit: 0.7.6 - language-tags: 2.1.0 - maxmind: 5.0.3 - playwright-core: 1.57.0 - progress: 2.0.3 - ua-parser-js: 2.0.7 - xml2js: 0.6.2 - - caniuse-lite@1.0.30001764: {} - chownr@1.1.4: {} + chownr@3.0.0: {} + + cloakbrowser@0.3.22(mmdb-lib@3.0.1)(playwright-core@1.59.1): + dependencies: + tar: 7.5.13 + optionalDependencies: + mmdb-lib: 3.0.1 + playwright-core: 1.59.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - commander@14.0.2: {} - cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 @@ -1102,22 +909,14 @@ snapshots: delayed-stream@1.0.0: {} - detect-europe-js@0.1.2: {} - detect-libc@2.1.2: {} - dot-prop@6.0.1: - dependencies: - is-obj: 2.0.0 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.267: {} - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -1139,47 +938,39 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild@0.27.2: + esbuild@0.27.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 - - escalade@3.2.0: {} + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 expand-template@2.0.3: {} file-uri-to-path@1.0.0: {} - fingerprint-generator@2.1.79: - dependencies: - generative-bayesian-network: 2.1.79 - header-generator: 2.1.79 - tslib: 2.8.1 - form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -1195,11 +986,6 @@ snapshots: function-bind@1.1.2: {} - generative-bayesian-network@2.1.79: - dependencies: - adm-zip: 0.5.16 - tslib: 2.8.1 - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1224,12 +1010,6 @@ snapshots: github-from-package@0.0.0: {} - glob@13.0.0: - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - gopd@1.2.0: {} has-symbols@1.1.0: {} @@ -1242,13 +1022,6 @@ snapshots: dependencies: function-bind: 1.1.2 - header-generator@2.1.79: - dependencies: - browserslist: 4.28.1 - generative-bayesian-network: 2.1.79 - ow: 0.28.2 - tslib: 2.8.1 - html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -1273,51 +1046,12 @@ snapshots: ieee754@1.2.1: {} - impit-darwin-arm64@0.7.6: - optional: true - - impit-darwin-x64@0.7.6: - optional: true - - impit-linux-arm64-gnu@0.7.6: - optional: true - - impit-linux-arm64-musl@0.7.6: - optional: true - - impit-linux-x64-gnu@0.7.6: - optional: true - - impit-linux-x64-musl@0.7.6: - optional: true - - impit-win32-arm64-msvc@0.7.6: - optional: true - - impit-win32-x64-msvc@0.7.6: - optional: true - - impit@0.7.6: - optionalDependencies: - impit-darwin-arm64: 0.7.6 - impit-darwin-x64: 0.7.6 - impit-linux-arm64-gnu: 0.7.6 - impit-linux-arm64-musl: 0.7.6 - impit-linux-x64-gnu: 0.7.6 - impit-linux-x64-musl: 0.7.6 - impit-win32-arm64-msvc: 0.7.6 - impit-win32-x64-msvc: 0.7.6 - inherits@2.0.4: {} ini@1.3.8: {} - is-obj@2.0.0: {} - is-potential-custom-element-name@1.0.1: {} - is-standalone-pwa@0.1.1: {} - jsdom@24.1.3: dependencies: cssstyle: 4.6.0 @@ -1346,25 +1080,10 @@ snapshots: - supports-color - utf-8-validate - language-subtag-registry@0.3.23: {} - - language-tags@2.1.0: - dependencies: - language-subtag-registry: 0.3.23 - - lodash.isequal@4.5.0: {} - lru-cache@10.4.3: {} - lru-cache@11.2.4: {} - math-intrinsics@1.1.0: {} - maxmind@5.0.3: - dependencies: - mmdb-lib: 3.0.1 - tiny-lru: 11.4.5 - mime-db@1.52.0: {} mime-types@2.1.35: @@ -1373,17 +1092,18 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimist@1.2.8: {} minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mkdirp-classic@0.5.3: {} - mmdb-lib@3.0.1: {} + mmdb-lib@3.0.1: + optional: true ms@2.1.3: {} @@ -1393,34 +1113,17 @@ snapshots: dependencies: semver: 7.7.3 - node-releases@2.0.27: {} - nwsapi@2.2.23: {} once@1.4.0: dependencies: wrappy: 1.0.2 - ow@0.28.2: - dependencies: - '@sindresorhus/is': 4.6.0 - callsites: 3.1.0 - dot-prop: 6.0.1 - lodash.isequal: 4.5.0 - vali-date: 1.0.0 - parse5@7.3.0: dependencies: entities: 6.0.1 - path-scurry@2.0.1: - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - - picocolors@1.1.1: {} - - playwright-core@1.57.0: {} + playwright-core@1.59.1: {} prebuild-install@7.1.3: dependencies: @@ -1437,8 +1140,6 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 - progress@2.0.3: {} - psl@1.15.0: dependencies: punycode: 2.3.1 @@ -1477,8 +1178,6 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.4: {} - saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -1516,7 +1215,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tiny-lru@11.4.5: {} + tar@7.5.13: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 tough-cookie@4.1.4: dependencies: @@ -1529,11 +1234,9 @@ snapshots: dependencies: punycode: 2.3.1 - tslib@2.8.1: {} - tsx@4.21.0: dependencies: - esbuild: 0.27.2 + esbuild: 0.27.0 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -1550,24 +1253,10 @@ snapshots: typescript@5.9.3: {} - ua-is-frozen@0.1.2: {} - - ua-parser-js@2.0.7: - dependencies: - detect-europe-js: 0.1.2 - is-standalone-pwa: 0.1.1 - ua-is-frozen: 0.1.2 - undici-types@7.16.0: {} universalify@0.2.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - url-parse@1.5.10: dependencies: querystringify: 2.2.0 @@ -1575,8 +1264,6 @@ snapshots: util-deprecate@1.0.2: {} - vali-date@1.0.0: {} - w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -1600,11 +1287,6 @@ snapshots: xml-name-validator@5.0.0: {} - xml2js@0.6.2: - dependencies: - sax: 1.4.4 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - xmlchars@2.2.0: {} + + yallist@5.0.0: {} diff --git a/skills/web-automation/claude-code/scripts/reference-source.json b/skills/web-automation/claude-code/scripts/reference-source.json new file mode 100644 index 0000000..e84ddeb --- /dev/null +++ b/skills/web-automation/claude-code/scripts/reference-source.json @@ -0,0 +1,24 @@ +{ + "referenceRepo": "https://git.fiorinis.com/Home/stef-openclaw-skills", + "referenceCommit": "b9878e938c1055e0284876aeb65157286d95f9d1", + "importedFiles": [ + "auth.ts", + "browse.ts", + "check-install.js", + "extract.js", + "flow.ts", + "package.json", + "scan-local-app.ts", + "scrape.ts", + "test-full.ts", + "test-minimal.ts", + "test-profile.ts" + ], + "excludedReferencePatterns": [ + "*discover.js", + "*photos.js", + "*identifiers.js", + "*.test.mjs", + "domain-specific helper scripts" + ] +} diff --git a/skills/web-automation/claude-code/scripts/scan-local-app.ts b/skills/web-automation/claude-code/scripts/scan-local-app.ts new file mode 100644 index 0000000..6a05b35 --- /dev/null +++ b/skills/web-automation/claude-code/scripts/scan-local-app.ts @@ -0,0 +1,174 @@ +#!/usr/bin/env npx tsx + +import { mkdirSync, writeFileSync } from 'fs'; +import { dirname, resolve } from 'path'; +import { getPage } from './browse.js'; + +type NavResult = { + requestedUrl: string; + url: string; + status: number | null; + title: string; + error?: string; +}; + +type RouteCheck = { + route: string; + result: NavResult; + heading: string | null; +}; + +const DEFAULT_BASE_URL = 'http://localhost:3000'; +const DEFAULT_REPORT_PATH = resolve(process.cwd(), 'scan-local-app.md'); + +function env(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + +function getRoutes(baseUrl: string): string[] { + const routeList = env('SCAN_ROUTES'); + if (routeList) { + return routeList + .split(',') + .map((route) => route.trim()) + .filter(Boolean) + .map((route) => new URL(route, baseUrl).toString()); + } + + return [baseUrl]; +} + +async function gotoWithStatus(page: any, url: string): Promise { + const response = await page + .goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }) + .catch((error: unknown) => ({ error })); + + if (response?.error) { + return { + requestedUrl: url, + url: page.url(), + status: null, + title: await page.title().catch(() => ''), + error: String(response.error), + }; + } + + return { + requestedUrl: url, + url: page.url(), + status: response ? response.status() : null, + title: await page.title().catch(() => ''), + }; +} + +async function textOrNull(page: any, selector: string): Promise { + const locator = page.locator(selector).first(); + try { + if ((await locator.count()) === 0) return null; + const value = await locator.textContent(); + return value ? value.trim().replace(/\s+/g, ' ') : null; + } catch { + return null; + } +} + +async function loginIfConfigured(page: any, baseUrl: string, lines: string[]) { + const loginPath = env('SCAN_LOGIN_PATH'); + const username = env('SCAN_USERNAME') ?? env('CLOAKBROWSER_USERNAME'); + const password = env('SCAN_PASSWORD') ?? env('CLOAKBROWSER_PASSWORD'); + const usernameSelector = env('SCAN_USERNAME_SELECTOR') ?? 'input[type="email"], input[name="email"]'; + const passwordSelector = env('SCAN_PASSWORD_SELECTOR') ?? 'input[type="password"], input[name="password"]'; + const submitSelector = env('SCAN_SUBMIT_SELECTOR') ?? 'button[type="submit"], input[type="submit"]'; + + if (!loginPath) { + lines.push('## Login'); + lines.push('- Skipped: set `SCAN_LOGIN_PATH` to enable login smoke checks.'); + lines.push(''); + return; + } + + const loginUrl = new URL(loginPath, baseUrl).toString(); + lines.push('## Login'); + lines.push(`- Login URL: ${loginUrl}`); + await gotoWithStatus(page, loginUrl); + + if (!username || !password) { + lines.push('- Skipped: set `SCAN_USERNAME`/`SCAN_PASSWORD` or `CLOAKBROWSER_USERNAME`/`CLOAKBROWSER_PASSWORD`.'); + lines.push(''); + return; + } + + await page.locator(usernameSelector).first().fill(username); + await page.locator(passwordSelector).first().fill(password); + await page.locator(submitSelector).first().click(); + await page.waitForTimeout(2500); + + lines.push(`- After submit URL: ${page.url()}`); + lines.push(`- Cookie count: ${(await page.context().cookies()).length}`); + lines.push(''); +} + +async function checkRoutes(page: any, baseUrl: string, lines: string[]) { + const routes = getRoutes(baseUrl); + const routeChecks: RouteCheck[] = []; + + for (const url of routes) { + const result = await gotoWithStatus(page, url); + const heading = await textOrNull(page, 'h1'); + routeChecks.push({ + route: url, + result, + heading, + }); + } + + lines.push('## Route Checks'); + for (const check of routeChecks) { + const relativeUrl = check.route.startsWith(baseUrl) ? check.route.slice(baseUrl.length) || '/' : check.route; + const finalPath = check.result.url.startsWith(baseUrl) + ? check.result.url.slice(baseUrl.length) || '/' + : check.result.url; + const suffix = check.heading ? `, h1="${check.heading}"` : ''; + const errorSuffix = check.result.error ? `, error="${check.result.error}"` : ''; + lines.push( + `- ${relativeUrl} → status ${check.result.status ?? 'ERR'} (final ${finalPath})${suffix}${errorSuffix}` + ); + } + lines.push(''); +} + +async function main() { + const baseUrl = env('SCAN_BASE_URL') ?? DEFAULT_BASE_URL; + const reportPath = resolve(env('SCAN_REPORT_PATH') ?? DEFAULT_REPORT_PATH); + const headless = (env('SCAN_HEADLESS') ?? env('CLOAKBROWSER_HEADLESS') ?? 'true') === 'true'; + const { page, browser } = await getPage({ headless }); + const lines: string[] = []; + + lines.push('# Web Automation Scan (local)'); + lines.push(''); + lines.push(`- Base URL: ${baseUrl}`); + lines.push(`- Timestamp: ${new Date().toISOString()}`); + lines.push(`- Headless: ${headless}`); + lines.push(`- Report Path: ${reportPath}`); + lines.push(''); + + try { + await loginIfConfigured(page, baseUrl, lines); + await checkRoutes(page, baseUrl, lines); + lines.push('## Notes'); + lines.push('- This generic smoke helper records route availability and top-level headings for a local app.'); + lines.push('- Configure login and route coverage with `SCAN_*` environment variables.'); + } finally { + await browser.close(); + } + + mkdirSync(dirname(reportPath), { recursive: true }); + writeFileSync(reportPath, `${lines.join('\n')}\n`, 'utf-8'); + console.log(`Report written to ${reportPath}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/skills/web-automation/claude-code/scripts/test-full.ts b/skills/web-automation/claude-code/scripts/test-full.ts index 42b30f4..356bbab 100644 --- a/skills/web-automation/claude-code/scripts/test-full.ts +++ b/skills/web-automation/claude-code/scripts/test-full.ts @@ -1,28 +1,25 @@ -import { Camoufox } from 'camoufox-js'; +import { launchPersistentContext } from 'cloakbrowser'; import { homedir } from 'os'; import { join } from 'path'; import { mkdirSync, existsSync } from 'fs'; async function test() { - const profilePath = join(homedir(), '.camoufox-profile'); + const profilePath = join(homedir(), '.cloakbrowser-profile'); if (!existsSync(profilePath)) { mkdirSync(profilePath, { recursive: true }); } console.log('Profile path:', profilePath); - console.log('Launching with full options...'); + console.log('Launching CloakBrowser with full options...'); - const browser = await Camoufox({ + const browser = await launchPersistentContext({ headless: true, - user_data_dir: profilePath, - // humanize: 1.5, // Test without this first - // geoip: true, // Test without this first - // enable_cache: true, - // block_webrtc: false, + userDataDir: profilePath, + humanize: true, }); console.log('Browser launched'); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); console.log('Page created'); await page.goto('https://github.com', { timeout: 30000 }); diff --git a/skills/web-automation/claude-code/scripts/test-minimal.ts b/skills/web-automation/claude-code/scripts/test-minimal.ts index 8f2f90a..a5412e7 100644 --- a/skills/web-automation/claude-code/scripts/test-minimal.ts +++ b/skills/web-automation/claude-code/scripts/test-minimal.ts @@ -1,10 +1,11 @@ -import { Camoufox } from 'camoufox-js'; +import { launch } from 'cloakbrowser'; async function test() { - console.log('Launching Camoufox with minimal config...'); + console.log('Launching CloakBrowser with minimal config...'); - const browser = await Camoufox({ + const browser = await launch({ headless: true, + humanize: true, }); console.log('Browser launched'); diff --git a/skills/web-automation/claude-code/scripts/test-profile.ts b/skills/web-automation/claude-code/scripts/test-profile.ts index 9afd227..ec59ddd 100644 --- a/skills/web-automation/claude-code/scripts/test-profile.ts +++ b/skills/web-automation/claude-code/scripts/test-profile.ts @@ -1,24 +1,25 @@ -import { Camoufox } from 'camoufox-js'; +import { launchPersistentContext } from 'cloakbrowser'; import { homedir } from 'os'; import { join } from 'path'; import { mkdirSync, existsSync } from 'fs'; async function test() { - const profilePath = join(homedir(), '.camoufox-profile'); + const profilePath = join(homedir(), '.cloakbrowser-profile'); if (!existsSync(profilePath)) { mkdirSync(profilePath, { recursive: true }); } console.log('Profile path:', profilePath); - console.log('Launching with user_data_dir...'); + console.log('Launching with persistent userDataDir...'); - const browser = await Camoufox({ + const browser = await launchPersistentContext({ headless: true, - user_data_dir: profilePath, + userDataDir: profilePath, + humanize: true, }); console.log('Browser launched'); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); console.log('Page created'); await page.goto('https://example.com', { timeout: 30000 }); diff --git a/skills/web-automation/opencode/SKILL.md b/skills/web-automation/opencode/SKILL.md index acbd687..7cccabf 100644 --- a/skills/web-automation/opencode/SKILL.md +++ b/skills/web-automation/opencode/SKILL.md @@ -1,48 +1,101 @@ --- name: web-automation -description: Browse and scrape web pages using Playwright with Camoufox anti-detection browser. Use when automating web workflows, extracting page content to markdown, handling authenticated sessions, or scraping websites with bot protection. +description: Browse and scrape web pages using Playwright-compatible CloakBrowser. Use when automating web workflows, extracting rendered page content, handling authenticated sessions, or running multi-step browser flows. --- -# Web Automation with Camoufox (OpenCode) +# Web Automation with CloakBrowser (OpenCode) -Automated web browsing and scraping using Playwright with Camoufox anti-detection browser. +Automated web browsing and scraping using Playwright-compatible CloakBrowser with two execution paths: + +- one-shot extraction via `extract.js` +- broader stateful automation via `auth.ts`, `browse.ts`, `flow.ts`, `scan-local-app.ts`, and `scrape.ts` ## Requirements - Node.js 20+ - pnpm -- Network access to download browser binaries +- Network access to download the CloakBrowser binary on first use ## First-Time Setup ```bash -cd ~/.opencode/skills/web-automation/scripts +cd ~/.config/opencode/skills/web-automation/scripts pnpm install -npx camoufox-js fetch +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild +``` + +## Updating CloakBrowser + +```bash +cd ~/.config/opencode/skills/web-automation/scripts +pnpm up cloakbrowser playwright-core +npx cloakbrowser install +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild ``` ## Prerequisite Check (MANDATORY) -Before running any automation, verify Playwright + Camoufox dependencies are installed and scripts are configured to use Camoufox. +Before running automation, verify CloakBrowser and Playwright Core are installed and wired correctly. ```bash -cd ~/.opencode/skills/web-automation/scripts -node -e "require.resolve('playwright-core/package.json');require.resolve('camoufox-js/package.json');console.log('OK: playwright-core + camoufox-js installed')" -node -e "const fs=require('fs');const t=fs.readFileSync('browse.ts','utf8');if(!/camoufox-js/.test(t)){throw new Error('browse.ts is not configured for Camoufox')}console.log('OK: Camoufox integration detected in browse.ts')" +cd ~/.config/opencode/skills/web-automation/scripts +node check-install.js ``` -If any check fails, stop and return: +`check-install.js` also prints the frozen reference repo + commit recorded in `reference-source.json`, so operators can confirm the canonical import source before using the skill. -"Missing dependency/config: web-automation requires `playwright-core` + `camoufox-js` and Camoufox-based scripts. Run setup in this skill, then retry." +If the check fails, stop and return: + +"Missing dependency/config: web-automation requires `cloakbrowser` and `playwright-core` with CloakBrowser-based scripts. Run setup in this skill, then retry." + +If runtime fails with missing native bindings for `better-sqlite3` or `esbuild`, run: + +```bash +cd ~/.config/opencode/skills/web-automation/scripts +pnpm approve-builds +pnpm rebuild better-sqlite3 esbuild +``` + +## When To Use Which Command + +- Use `node extract.js ""` for a one-shot rendered fetch with JSON output. +- Use `npx tsx scrape.ts ...` when you need markdown extraction, Readability cleanup, or selector-based scraping. +- Use `npx tsx browse.ts ...`, `auth.ts`, or `flow.ts` when the task needs login handling, persistent sessions, clicks, typing, screenshots, or multi-step navigation. +- Use `npx tsx scan-local-app.ts` when you need a configurable local-app smoke pass driven by `SCAN_*` and `CLOAKBROWSER_*` environment variables. ## Quick Reference +- Install check: `node check-install.js` +- One-shot JSON extract: `node extract.js "https://example.com"` - Browse page: `npx tsx browse.ts --url "https://example.com"` - Scrape markdown: `npx tsx scrape.ts --url "https://example.com" --mode main --output page.md` - Authenticate: `npx tsx auth.ts --url "https://example.com/login"` +- Natural-language flow: `npx tsx flow.ts --instruction 'go to https://example.com then click on "Login" then type "user@example.com" in #email then press enter'` +- Local app smoke scan: `SCAN_BASE_URL=http://localhost:3000 SCAN_ROUTES=/,/dashboard npx tsx scan-local-app.ts` + +## Local App Smoke Scan + +`scan-local-app.ts` is intentionally generic. Configure it with environment variables instead of editing the file: + +- `SCAN_BASE_URL` +- `SCAN_LOGIN_PATH` +- `SCAN_USERNAME` +- `SCAN_PASSWORD` +- `SCAN_USERNAME_SELECTOR` +- `SCAN_PASSWORD_SELECTOR` +- `SCAN_SUBMIT_SELECTOR` +- `SCAN_ROUTES` +- `SCAN_REPORT_PATH` +- `SCAN_HEADLESS` + +If `SCAN_USERNAME` or `SCAN_PASSWORD` are omitted, the script falls back to `CLOAKBROWSER_USERNAME` and `CLOAKBROWSER_PASSWORD`. ## Notes -- Sessions persist in Camoufox profile storage. +- Sessions persist in CloakBrowser profile storage. - Use `--wait` for dynamic pages. - Use `--mode selector --selector "..."` for targeted extraction. +- `extract.js` keeps a bounded stealth/rendered fetch path without needing a long-lived automation session. diff --git a/skills/web-automation/opencode/scripts/auth.ts b/skills/web-automation/opencode/scripts/auth.ts index 5c25b98..e79f23d 100644 --- a/skills/web-automation/opencode/scripts/auth.ts +++ b/skills/web-automation/opencode/scripts/auth.ts @@ -41,8 +41,8 @@ function getCredentials(options?: { username?: string; password?: string; }): { username: string; password: string } | null { - const username = options?.username || process.env.CAMOUFOX_USERNAME; - const password = options?.password || process.env.CAMOUFOX_PASSWORD; + const username = options?.username || process.env.CLOAKBROWSER_USERNAME; + const password = options?.password || process.env.CLOAKBROWSER_PASSWORD; if (!username || !password) { return null; @@ -450,7 +450,7 @@ export async function navigateAuthenticated( if (!credentials) { throw new Error( 'Authentication required but no credentials provided. ' + - 'Set CAMOUFOX_USERNAME and CAMOUFOX_PASSWORD environment variables.' + 'Set CLOAKBROWSER_USERNAME and CLOAKBROWSER_PASSWORD environment variables.' ); } @@ -504,8 +504,8 @@ Usage: Options: -u, --url URL to authenticate (required) -t, --type Auth type: auto, form, or msal (default: auto) - --username Username/email (or set CAMOUFOX_USERNAME env var) - --password Password (or set CAMOUFOX_PASSWORD env var) + --username Username/email (or set CLOAKBROWSER_USERNAME env var) + --password Password (or set CLOAKBROWSER_PASSWORD env var) --headless Run in headless mode (default: false for auth) -h, --help Show this help message @@ -515,8 +515,8 @@ Auth Types: msal Microsoft SSO (login.microsoftonline.com) Environment Variables: - CAMOUFOX_USERNAME Default username/email for authentication - CAMOUFOX_PASSWORD Default password for authentication + CLOAKBROWSER_USERNAME Default username/email for authentication + CLOAKBROWSER_PASSWORD Default password for authentication Examples: # Interactive login (no credentials, opens browser) @@ -527,11 +527,11 @@ Examples: --username "user@example.com" --password "secret" # Microsoft SSO login - CAMOUFOX_USERNAME=user@company.com CAMOUFOX_PASSWORD=secret \\ + CLOAKBROWSER_USERNAME=user@company.com CLOAKBROWSER_PASSWORD=secret \\ npx tsx auth.ts --url "https://internal.company.com" --type msal Notes: - - Session is saved to ~/.camoufox-profile/ for persistence + - Session is saved to ~/.cloakbrowser-profile/ for persistence - After successful auth, subsequent browses will be authenticated - Use --headless false if you need to handle MFA manually `); diff --git a/skills/web-automation/opencode/scripts/browse.ts b/skills/web-automation/opencode/scripts/browse.ts index 901089b..01cf098 100644 --- a/skills/web-automation/opencode/scripts/browse.ts +++ b/skills/web-automation/opencode/scripts/browse.ts @@ -1,7 +1,7 @@ #!/usr/bin/env npx tsx /** - * Browser launcher using Camoufox with persistent profile + * Browser launcher using CloakBrowser with persistent profile * * Usage: * npx tsx browse.ts --url "https://example.com" @@ -9,14 +9,13 @@ * npx tsx browse.ts --url "https://example.com" --headless false --wait 5000 */ -import { Camoufox } from 'camoufox-js'; +import { launchPersistentContext } from 'cloakbrowser'; import { homedir } from 'os'; import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; import parseArgs from 'minimist'; import type { Page, BrowserContext } from 'playwright-core'; -// Types interface BrowseOptions { url: string; headless?: boolean; @@ -33,55 +32,54 @@ interface BrowseResult { screenshotPath?: string; } -// Get profile directory +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + const getProfilePath = (): string => { - const customPath = process.env.CAMOUFOX_PROFILE_PATH; + const customPath = process.env.CLOAKBROWSER_PROFILE_PATH; if (customPath) return customPath; - const profileDir = join(homedir(), '.camoufox-profile'); + const profileDir = join(homedir(), '.cloakbrowser-profile'); if (!existsSync(profileDir)) { mkdirSync(profileDir, { recursive: true }); } return profileDir; }; -// Launch browser with persistent profile export async function launchBrowser(options: { headless?: boolean; }): Promise { const profilePath = getProfilePath(); - const headless = - options.headless ?? - (process.env.CAMOUFOX_HEADLESS ? process.env.CAMOUFOX_HEADLESS === 'true' : true); + const envHeadless = process.env.CLOAKBROWSER_HEADLESS; + const headless = options.headless ?? (envHeadless ? envHeadless === 'true' : true); console.log(`Using profile: ${profilePath}`); console.log(`Headless mode: ${headless}`); - const browser = await Camoufox({ - user_data_dir: profilePath, + const context = await launchPersistentContext({ + userDataDir: profilePath, headless, + humanize: true, }); - return browser; + return context; } -// Browse to URL and optionally take screenshot export async function browse(options: BrowseOptions): Promise { const browser = await launchBrowser({ headless: options.headless }); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); try { - // Navigate to URL console.log(`Navigating to: ${options.url}`); await page.goto(options.url, { timeout: options.timeout ?? 60000, waitUntil: 'domcontentloaded', }); - // Wait if specified if (options.wait) { console.log(`Waiting ${options.wait}ms...`); - await page.waitForTimeout(options.wait); + await sleep(options.wait); } const result: BrowseResult = { @@ -92,7 +90,6 @@ export async function browse(options: BrowseOptions): Promise { console.log(`Page title: ${result.title}`); console.log(`Final URL: ${result.url}`); - // Take screenshot if requested if (options.screenshot) { const outputPath = options.output ?? 'screenshot.png'; await page.screenshot({ path: outputPath, fullPage: true }); @@ -100,11 +97,10 @@ export async function browse(options: BrowseOptions): Promise { console.log(`Screenshot saved: ${outputPath}`); } - // If interactive mode, keep browser open if (options.interactive) { console.log('\nInteractive mode - browser will stay open.'); console.log('Press Ctrl+C to close.'); - await new Promise(() => {}); // Keep running + await new Promise(() => {}); } return result; @@ -115,16 +111,14 @@ export async function browse(options: BrowseOptions): Promise { } } -// Export page for use in other scripts export async function getPage(options?: { headless?: boolean; }): Promise<{ page: Page; browser: BrowserContext }> { const browser = await launchBrowser({ headless: options?.headless }); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); return { page, browser }; } -// CLI entry point async function main() { const args = parseArgs(process.argv.slice(2), { string: ['url', 'output'], @@ -145,7 +139,7 @@ async function main() { if (args.help || !args.url) { console.log(` -Web Browser with Camoufox +Web Browser with CloakBrowser Usage: npx tsx browse.ts --url [options] @@ -166,8 +160,8 @@ Examples: npx tsx browse.ts --url "https://example.com" --headless false --interactive Environment Variables: - CAMOUFOX_PROFILE_PATH Custom profile directory (default: ~/.camoufox-profile/) - CAMOUFOX_HEADLESS Default headless mode (true/false) + CLOAKBROWSER_PROFILE_PATH Custom profile directory (default: ~/.cloakbrowser-profile/) + CLOAKBROWSER_HEADLESS Default headless mode (true/false) `); process.exit(args.help ? 0 : 1); } @@ -188,7 +182,6 @@ Environment Variables: } } -// Run if executed directly const isMainModule = process.argv[1]?.includes('browse.ts'); if (isMainModule) { main(); diff --git a/skills/web-automation/opencode/scripts/check-install.js b/skills/web-automation/opencode/scripts/check-install.js new file mode 100644 index 0000000..9423779 --- /dev/null +++ b/skills/web-automation/opencode/scripts/check-install.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const referencePath = path.join(__dirname, "reference-source.json"); + +function fail(message, details) { + const payload = { error: message }; + if (details) payload.details = details; + process.stderr.write(`${JSON.stringify(payload)}\n`); + process.exit(1); +} + +async function main() { + try { + await import("cloakbrowser"); + await import("playwright-core"); + } catch (error) { + fail( + "Missing dependency/config: web-automation requires cloakbrowser and playwright-core.", + error instanceof Error ? error.message : String(error) + ); + } + + const browsePath = path.join(__dirname, "browse.ts"); + const browseSource = fs.readFileSync(browsePath, "utf8"); + if (!/launchPersistentContext/.test(browseSource) || !/from ['"]cloakbrowser['"]/.test(browseSource)) { + fail("browse.ts is not configured for CloakBrowser."); + } + + const referenceSource = JSON.parse(fs.readFileSync(referencePath, "utf8")); + if (!referenceSource.referenceRepo || !referenceSource.referenceCommit) { + fail("Frozen reference metadata is missing from reference-source.json."); + } + + process.stdout.write("OK: cloakbrowser + playwright-core installed\n"); + process.stdout.write("OK: CloakBrowser integration detected in browse.ts\n"); + process.stdout.write( + `OK: frozen reference ${referenceSource.referenceRepo}@${referenceSource.referenceCommit}\n` + ); +} + +main().catch((error) => { + fail("Install check failed.", error instanceof Error ? error.message : String(error)); +}); diff --git a/skills/web-automation/opencode/scripts/extract.js b/skills/web-automation/opencode/scripts/extract.js new file mode 100755 index 0000000..5e3908a --- /dev/null +++ b/skills/web-automation/opencode/scripts/extract.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const DEFAULT_WAIT_MS = 5000; +const MAX_WAIT_MS = 20000; +const NAV_TIMEOUT_MS = 30000; +const EXTRA_CHALLENGE_WAIT_MS = 8000; +const CONTENT_LIMIT = 12000; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function fail(message, details) { + const payload = { error: message }; + if (details) payload.details = details; + process.stderr.write(`${JSON.stringify(payload)}\n`); + process.exit(1); +} + +function parseWaitTime(raw) { + const value = Number.parseInt(raw || `${DEFAULT_WAIT_MS}`, 10); + if (!Number.isFinite(value) || value < 0) return DEFAULT_WAIT_MS; + return Math.min(value, MAX_WAIT_MS); +} + +function parseTarget(rawUrl) { + if (!rawUrl) { + fail("Missing URL. Usage: node extract.js "); + } + + let parsed; + try { + parsed = new URL(rawUrl); + } catch (error) { + fail("Invalid URL.", error.message); + } + + if (!["http:", "https:"].includes(parsed.protocol)) { + fail("Only http and https URLs are allowed."); + } + + return parsed.toString(); +} + +function ensureParentDir(filePath) { + if (!filePath) return; + fs.mkdirSync(path.dirname(filePath), { recursive: true }); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function detectChallenge(page) { + try { + return await page.evaluate(() => { + const text = (document.body?.innerText || "").toLowerCase(); + return ( + text.includes("checking your browser") || + text.includes("just a moment") || + text.includes("verify you are human") || + text.includes("press and hold") || + document.querySelector('iframe[src*="challenge"]') !== null || + document.querySelector('iframe[src*="cloudflare"]') !== null + ); + }); + } catch { + return false; + } +} + +async function loadCloakBrowser() { + try { + return await import("cloakbrowser"); + } catch (error) { + fail( + "CloakBrowser is not installed for this skill. Run pnpm install in this skill's scripts directory first.", + error.message + ); + } +} + +async function runWithStderrLogs(fn) { + const originalLog = console.log; + const originalError = console.error; + console.log = (...args) => process.stderr.write(`${args.join(" ")}\n`); + console.error = (...args) => process.stderr.write(`${args.join(" ")}\n`); + try { + return await fn(); + } finally { + console.log = originalLog; + console.error = originalError; + } +} + +async function main() { + const requestedUrl = parseTarget(process.argv[2]); + const waitTime = parseWaitTime(process.env.WAIT_TIME); + const screenshotPath = process.env.SCREENSHOT_PATH || ""; + const saveHtml = process.env.SAVE_HTML === "true"; + const headless = process.env.HEADLESS !== "false"; + const userAgent = process.env.USER_AGENT || undefined; + const startedAt = Date.now(); + const { ensureBinary, launchContext } = await loadCloakBrowser(); + + let context; + try { + await runWithStderrLogs(() => ensureBinary()); + + context = await runWithStderrLogs(() => launchContext({ + headless, + userAgent, + locale: "en-US", + viewport: { width: 1440, height: 900 }, + humanize: true, + })); + + const page = await context.newPage(); + const response = await page.goto(requestedUrl, { + waitUntil: "domcontentloaded", + timeout: NAV_TIMEOUT_MS + }); + + await sleep(waitTime); + + let challengeDetected = await detectChallenge(page); + if (challengeDetected) { + await sleep(EXTRA_CHALLENGE_WAIT_MS); + challengeDetected = await detectChallenge(page); + } + + const extracted = await page.evaluate((contentLimit) => { + const bodyText = document.body?.innerText || ""; + return { + finalUrl: window.location.href, + title: document.title || "", + content: bodyText.slice(0, contentLimit), + metaDescription: + document.querySelector('meta[name="description"]')?.content || + document.querySelector('meta[property="og:description"]')?.content || + "" + }; + }, CONTENT_LIMIT); + + const result = { + requestedUrl, + finalUrl: extracted.finalUrl, + title: extracted.title, + content: extracted.content, + metaDescription: extracted.metaDescription, + status: response ? response.status() : null, + challengeDetected, + elapsedSeconds: ((Date.now() - startedAt) / 1000).toFixed(2) + }; + + if (screenshotPath) { + ensureParentDir(screenshotPath); + await page.screenshot({ path: screenshotPath, fullPage: false, timeout: 10000 }); + result.screenshot = screenshotPath; + } + + if (saveHtml) { + const htmlTarget = screenshotPath + ? screenshotPath.replace(/\.[^.]+$/, ".html") + : path.resolve(__dirname, `page-${Date.now()}.html`); + ensureParentDir(htmlTarget); + fs.writeFileSync(htmlTarget, await page.content()); + result.htmlFile = htmlTarget; + } + + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + await context.close(); + } catch (error) { + if (context) { + try { + await context.close(); + } catch { + // Ignore close errors after the primary failure. + } + } + fail("Scrape failed.", error.message); + } +} + +main(); diff --git a/skills/web-automation/opencode/scripts/flow.ts b/skills/web-automation/opencode/scripts/flow.ts new file mode 100644 index 0000000..5d01e55 --- /dev/null +++ b/skills/web-automation/opencode/scripts/flow.ts @@ -0,0 +1,329 @@ +#!/usr/bin/env npx tsx + +import parseArgs from 'minimist'; +import type { Page } from 'playwright-core'; +import { launchBrowser } from './browse'; + +type Step = + | { action: 'goto'; url: string } + | { action: 'click'; selector?: string; text?: string; role?: string; name?: string } + | { action: 'type'; selector?: string; text: string } + | { action: 'press'; key: string; selector?: string } + | { action: 'wait'; ms: number } + | { action: 'screenshot'; path: string } + | { action: 'extract'; selector: string; count?: number }; + +function normalizeNavigationUrl(rawUrl: string): string { + let parsed: URL; + try { + parsed = new URL(rawUrl); + } catch { + throw new Error(`Invalid navigation URL: ${rawUrl}`); + } + + if (!['http:', 'https:'].includes(parsed.protocol)) { + throw new Error(`Only http and https URLs are allowed in flow steps: ${rawUrl}`); + } + + return parsed.toString(); +} + +function normalizeKey(k: string): string { + if (!k) return 'Enter'; + const lower = k.toLowerCase(); + if (lower === 'enter' || lower === 'return') return 'Enter'; + if (lower === 'tab') return 'Tab'; + if (lower === 'escape' || lower === 'esc') return 'Escape'; + return k; +} + +function splitInstructions(instruction: string): string[] { + return instruction + .split(/\bthen\b|;/gi) + .map((s) => s.trim()) + .filter(Boolean); +} + +function parseInstruction(instruction: string): Step[] { + const parts = splitInstructions(instruction); + const steps: Step[] = []; + + for (const p of parts) { + // go to https://... + const goto = p.match(/^(?:go to|open|navigate to)\s+(https?:\/\/\S+)/i); + if (goto) { + steps.push({ action: 'goto', url: normalizeNavigationUrl(goto[1]) }); + continue; + } + + // click on "text" or click #selector or click button "name" + const clickRole = p.match(/^click\s+(button|link|textbox|img|image|tab)\s+"([^"]+)"$/i); + if (clickRole) { + const role = clickRole[1].toLowerCase() === 'image' ? 'img' : clickRole[1].toLowerCase(); + steps.push({ action: 'click', role, name: clickRole[2] }); + continue; + } + const clickText = p.match(/^click(?: on)?\s+"([^"]+)"/i); + if (clickText) { + steps.push({ action: 'click', text: clickText[1] }); + continue; + } + const clickSelector = p.match(/^click(?: on)?\s+(#[\w-]+|\.[\w-]+|[a-z]+\[[^\]]+\])/i); + if (clickSelector) { + steps.push({ action: 'click', selector: clickSelector[1] }); + continue; + } + + // type "text" [in selector] + const typeInto = p.match(/^type\s+"([^"]+)"\s+in\s+(.+)$/i); + if (typeInto) { + steps.push({ action: 'type', text: typeInto[1], selector: typeInto[2].trim() }); + continue; + } + const typeOnly = p.match(/^type\s+"([^"]+)"$/i); + if (typeOnly) { + steps.push({ action: 'type', text: typeOnly[1] }); + continue; + } + + // press enter [in selector] + const pressIn = p.match(/^press\s+(\w+)\s+in\s+(.+)$/i); + if (pressIn) { + steps.push({ action: 'press', key: normalizeKey(pressIn[1]), selector: pressIn[2].trim() }); + continue; + } + const pressOnly = p.match(/^press\s+(\w+)$/i); + if (pressOnly) { + steps.push({ action: 'press', key: normalizeKey(pressOnly[1]) }); + continue; + } + + // wait 2s / wait 500ms + const waitS = p.match(/^wait\s+(\d+)\s*s(?:ec(?:onds?)?)?$/i); + if (waitS) { + steps.push({ action: 'wait', ms: parseInt(waitS[1], 10) * 1000 }); + continue; + } + const waitMs = p.match(/^wait\s+(\d+)\s*ms$/i); + if (waitMs) { + steps.push({ action: 'wait', ms: parseInt(waitMs[1], 10) }); + continue; + } + + // screenshot path + const shot = p.match(/^screenshot(?: to)?\s+(.+)$/i); + if (shot) { + steps.push({ action: 'screenshot', path: shot[1].trim() }); + continue; + } + + throw new Error(`Could not parse step: "${p}"`); + } + + return steps; +} + +function validateSteps(steps: Step[]): Step[] { + return steps.map((step) => + step.action === 'goto' + ? { + ...step, + url: normalizeNavigationUrl(step.url), + } + : step + ); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function isLikelyLoginText(text: string): boolean { + return /(login|accedi|sign\s*in|entra)/i.test(text); +} + +async function clickByText(page: Page, text: string): Promise { + const patterns = [new RegExp(`^${escapeRegExp(text)}$`, 'i'), new RegExp(escapeRegExp(text), 'i')]; + + for (const pattern of patterns) { + const targets = [ + page.getByRole('button', { name: pattern }).first(), + page.getByRole('link', { name: pattern }).first(), + page.getByText(pattern).first(), + ]; + + for (const target of targets) { + if (await target.count()) { + try { + await target.click({ timeout: 8000 }); + return true; + } catch { + // keep trying next candidate + } + } + } + } + + return false; +} + +async function fallbackLoginNavigation(page: Page, requestedText: string): Promise { + if (!isLikelyLoginText(requestedText)) return false; + + const current = new URL(page.url()); + + const candidateLinks = await page.evaluate(() => { + const loginTerms = ['login', 'accedi', 'sign in', 'entra']; + const anchors = Array.from(document.querySelectorAll('a[href], a[onclick], button[onclick]')) as Array; + + return anchors + .map((el) => { + const text = (el.textContent || '').trim().toLowerCase(); + const href = (el as HTMLAnchorElement).getAttribute('href') || ''; + return { text, href }; + }) + .filter((x) => x.text && loginTerms.some((t) => x.text.includes(t))) + .map((x) => x.href) + .filter(Boolean); + }); + + // Prefer real URLs (not javascript:) + const realCandidate = candidateLinks.find((h) => /login|account\/login/i.test(h) && !h.startsWith('javascript:')); + if (realCandidate) { + const target = new URL(realCandidate, page.url()).toString(); + await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60000 }); + return true; + } + + // Site-specific fallback for Corriere + if (/corriere\.it$/i.test(current.hostname) || /\.corriere\.it$/i.test(current.hostname)) { + await page.goto('https://www.corriere.it/account/login', { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + return true; + } + + return false; +} + +async function typeInBestTarget(page: Page, text: string, selector?: string) { + if (selector) { + await page.locator(selector).first().click({ timeout: 10000 }); + await page.locator(selector).first().fill(text); + return; + } + const loc = page.locator('input[name="q"], input[type="search"], input[type="text"], textarea').first(); + await loc.click({ timeout: 10000 }); + await loc.fill(text); +} + +async function pressOnTarget(page: Page, key: string, selector?: string) { + if (selector) { + await page.locator(selector).first().press(key); + return; + } + await page.keyboard.press(key); +} + +async function runSteps(page: Page, steps: Step[]) { + for (const step of steps) { + switch (step.action) { + case 'goto': + await page.goto(normalizeNavigationUrl(step.url), { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + break; + case 'click': + if (step.selector) { + await page.locator(step.selector).first().click({ timeout: 15000 }); + } else if (step.role && step.name) { + await page.getByRole(step.role as any, { name: new RegExp(escapeRegExp(step.name), 'i') }).first().click({ timeout: 15000 }); + } else if (step.text) { + const clicked = await clickByText(page, step.text); + if (!clicked) { + const recovered = await fallbackLoginNavigation(page, step.text); + if (!recovered) { + throw new Error(`Could not click target text: ${step.text}`); + } + } + } else { + throw new Error('click step missing selector/text/role'); + } + try { + await page.waitForLoadState('domcontentloaded', { timeout: 10000 }); + } catch { + // no navigation is fine + } + break; + case 'type': + await typeInBestTarget(page, step.text, step.selector); + break; + case 'press': + await pressOnTarget(page, step.key, step.selector); + break; + case 'wait': + await page.waitForTimeout(step.ms); + break; + case 'screenshot': + await page.screenshot({ path: step.path, fullPage: true }); + break; + case 'extract': { + const items = await page.locator(step.selector).allTextContents(); + const out = items.slice(0, step.count ?? items.length).map((t) => t.trim()).filter(Boolean); + console.log(JSON.stringify(out, null, 2)); + break; + } + default: + throw new Error('Unknown step'); + } + } +} + +async function main() { + const args = parseArgs(process.argv.slice(2), { + string: ['instruction', 'steps'], + boolean: ['headless', 'help'], + default: { headless: true }, + alias: { i: 'instruction', s: 'steps', h: 'help' }, + }); + + if (args.help || (!args.instruction && !args.steps)) { + console.log(` +General Web Flow Runner (CloakBrowser) + +Usage: + npx tsx flow.ts --instruction "go to https://example.com then type \"hello\" then press enter" + npx tsx flow.ts --steps '[{"action":"goto","url":"https://example.com"}]' + +Supported natural steps: + - go to/open/navigate to + - click on "Text" + - click + - type "text" + - type "text" in + - press + - press in + - wait s | wait ms + - screenshot +`); + process.exit(args.help ? 0 : 1); + } + + const steps = validateSteps(args.steps ? JSON.parse(args.steps) : parseInstruction(args.instruction)); + const browser = await launchBrowser({ headless: args.headless }); + const page = await browser.newPage(); + + try { + await runSteps(page, steps); + console.log('Flow complete. Final URL:', page.url()); + } finally { + await browser.close(); + } +} + +main().catch((e) => { + console.error('Error:', e instanceof Error ? e.message : e); + process.exit(1); +}); diff --git a/skills/web-automation/opencode/scripts/package.json b/skills/web-automation/opencode/scripts/package.json index 3beb74d..a2221e8 100644 --- a/skills/web-automation/opencode/scripts/package.json +++ b/skills/web-automation/opencode/scripts/package.json @@ -1,19 +1,26 @@ { "name": "web-automation-scripts", "version": "1.0.0", - "description": "Web browsing and scraping scripts using Camoufox", + "description": "Web browsing and scraping scripts using CloakBrowser", "type": "module", "scripts": { + "check-install": "node check-install.js", + "extract": "node extract.js", "browse": "tsx browse.ts", + "auth": "tsx auth.ts", + "flow": "tsx flow.ts", "scrape": "tsx scrape.ts", - "fetch-browser": "npx camoufox-js fetch" + "typecheck": "tsc --noEmit -p tsconfig.json", + "lint": "pnpm run typecheck && node --check check-install.js && node --check extract.js", + "fetch-browser": "npx cloakbrowser install" }, "dependencies": { "@mozilla/readability": "^0.5.0", - "camoufox-js": "^0.8.5", + "better-sqlite3": "^12.6.2", + "cloakbrowser": "^0.3.22", "jsdom": "^24.0.0", "minimist": "^1.2.8", - "playwright-core": "^1.40.0", + "playwright-core": "^1.59.1", "turndown": "^7.1.2", "turndown-plugin-gfm": "^1.0.2" }, diff --git a/skills/web-automation/opencode/scripts/pnpm-lock.yaml b/skills/web-automation/opencode/scripts/pnpm-lock.yaml index 20d8e27..59dba9c 100644 --- a/skills/web-automation/opencode/scripts/pnpm-lock.yaml +++ b/skills/web-automation/opencode/scripts/pnpm-lock.yaml @@ -11,9 +11,12 @@ importers: '@mozilla/readability': specifier: ^0.5.0 version: 0.5.0 - camoufox-js: - specifier: ^0.8.5 - version: 0.8.5(playwright-core@1.57.0) + better-sqlite3: + specifier: ^12.6.2 + version: 12.8.0 + cloakbrowser: + specifier: ^0.3.22 + version: 0.3.22(mmdb-lib@3.0.1)(playwright-core@1.59.1) jsdom: specifier: ^24.0.0 version: 24.1.3 @@ -21,8 +24,8 @@ importers: specifier: ^1.2.8 version: 1.2.8 playwright-core: - specifier: ^1.40.0 - version: 1.57.0 + specifier: ^1.59.1 + version: 1.59.1 turndown: specifier: ^7.1.2 version: 7.2.2 @@ -238,13 +241,9 @@ packages: cpu: [x64] os: [win32] - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -253,10 +252,6 @@ packages: resolution: {integrity: sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==} engines: {node: '>=14.0.0'} - '@sindresorhus/is@4.6.0': - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - '@types/jsdom@21.1.7': resolution: {integrity: sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==} @@ -272,10 +267,6 @@ packages: '@types/turndown@5.0.6': resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} - adm-zip@0.5.16: - resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} - engines: {node: '>=12.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -286,12 +277,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.14: - resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} - hasBin: true - - better-sqlite3@12.6.0: - resolution: {integrity: sha512-FXI191x+D6UPWSze5IzZjhz+i9MK9nsuHsmTX9bXVl52k06AfZ2xql0lrgIUuzsMsJ7Vgl5kIptvDgBLIV3ZSQ==} + better-sqlite3@12.8.0: + resolution: {integrity: sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==} engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} bindings@1.5.0: @@ -300,11 +287,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -312,31 +294,33 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camoufox-js@0.8.5: - resolution: {integrity: sha512-20ihPbspAcOVSUTX9Drxxp0C116DON1n8OVA1eUDglWZiHwiHwFVFOMrIEBwAHMZpU11mIEH/kawJtstRIrDPA==} - engines: {node: '>= 20'} - hasBin: true - peerDependencies: - playwright-core: '*' - - caniuse-lite@1.0.30001764: - resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} - chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cloakbrowser@0.3.22: + resolution: {integrity: sha512-L2CWQiVdunhKslTli8HCe4INhaAt4npbvsM2Ox4/idqiRmT2BADndQ05eDS8TonNSWeWqbjsh04UhSZOD3B6mg==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + mmdb-lib: '>=2.0.0' + playwright-core: '>=1.40.0' + puppeteer-core: '>=21.0.0' + peerDependenciesMeta: + mmdb-lib: + optional: true + playwright-core: + optional: true + puppeteer-core: + optional: true + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} - commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} - engines: {node: '>=20'} - cssstyle@4.6.0: resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} engines: {node: '>=18'} @@ -369,24 +353,14 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - detect-europe-js@0.1.2: - resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dot-prop@6.0.1: - resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} - engines: {node: '>=10'} - dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.267: - resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -415,10 +389,6 @@ packages: engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -426,10 +396,6 @@ packages: file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fingerprint-generator@2.1.79: - resolution: {integrity: sha512-0dr3kTgvRYHleRPp6OBDcPb8amJmOyFr9aOuwnpN6ooWJ5XyT+/aL/SZ6CU4ZrEtzV26EyJ2Lg7PT32a0NdrRA==} - engines: {node: '>=16.0.0'} - form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -445,9 +411,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - generative-bayesian-network@2.1.79: - resolution: {integrity: sha512-aPH+V2wO+HE0BUX1LbsM8Ak99gmV43lgh+D7GDteM0zgnPqiAwcK9JZPxMPZa3aJUleFtFaL1lAei8g9zNrDIA==} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -462,10 +425,6 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -482,10 +441,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - header-generator@2.1.79: - resolution: {integrity: sha512-YvHx8teq4QmV5mz7wdPMsj9n1OZBPnZxA4QE+EOrtx7xbmGvd1gBvDNKCb5XqS4GR/TL75MU5hqMqqqANdILRg==} - engines: {node: '>=16.0.0'} - html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -505,74 +460,15 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - impit-darwin-arm64@0.7.6: - resolution: {integrity: sha512-M7NQXkttyzqilWfzVkNCp7hApT69m0etyJkVpHze4bR5z1kJnHhdsb8BSdDv2dzvZL4u1JyqZNxq+qoMn84eUw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - impit-darwin-x64@0.7.6: - resolution: {integrity: sha512-kikTesWirAwJp9JPxzGLoGVc+heBlEabWS5AhTkQedACU153vmuL90OBQikVr3ul2N0LPImvnuB+51wV0zDE6g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - impit-linux-arm64-gnu@0.7.6: - resolution: {integrity: sha512-H6GHjVr/0lG9VEJr6IHF8YLq+YkSIOF4k7Dfue2ygzUAj1+jZ5ZwnouhG/XrZHYW6EWsZmEAjjRfWE56Q0wDRQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - impit-linux-arm64-musl@0.7.6: - resolution: {integrity: sha512-1sCB/UBVXLZTpGJsXRdNNSvhN9xmmQcYLMWAAB4Itb7w684RHX1pLoCb6ichv7bfAf6tgaupcFIFZNBp3ghmQA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - impit-linux-x64-gnu@0.7.6: - resolution: {integrity: sha512-yYhlRnZ4fhKt8kuGe0JK2WSHc8TkR6BEH0wn+guevmu8EOn9Xu43OuRvkeOyVAkRqvFnlZtMyySUo/GuSLz9Gw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - impit-linux-x64-musl@0.7.6: - resolution: {integrity: sha512-sdGWyu+PCLmaOXy7Mzo4WP61ZLl5qpZ1L+VeXW+Ycazgu0e7ox0NZLdiLRunIrEzD+h0S+e4CyzNwaiP3yIolg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - impit-win32-arm64-msvc@0.7.6: - resolution: {integrity: sha512-sM5deBqo0EuXg5GACBUMKEua9jIau/i34bwNlfrf/Amnw1n0GB4/RkuUh+sKiUcbNAntrRq+YhCq8qDP8IW19w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - impit-win32-x64-msvc@0.7.6: - resolution: {integrity: sha512-ry63ADGLCB/PU/vNB1VioRt2V+klDJ34frJUXUZBEv1kA96HEAg9AxUk+604o+UHS3ttGH2rkLmrbwHOdAct5Q==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - impit@0.7.6: - resolution: {integrity: sha512-AkS6Gv63+E6GMvBrcRhMmOREKpq5oJ0J5m3xwfkHiEs97UIsbpEqFmW3sFw/sdyOTDGRF5q4EjaLxtb922Ta8g==} - engines: {node: '>= 20'} - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-standalone-pwa@0.1.1: - resolution: {integrity: sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==} - jsdom@24.1.3: resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} engines: {node: '>=18'} @@ -582,32 +478,13 @@ packages: canvas: optional: true - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@2.1.0: - resolution: {integrity: sha512-D4CgpyCt+61f6z2jHjJS1OmZPviAWM57iJ9OKdFFWSNgS7Udj9QVWqyGs/cveVNF57XpZmhSvMdVIV5mjLA7Vg==} - engines: {node: '>=22'} - - lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - maxmind@5.0.3: - resolution: {integrity: sha512-oMtZwLrsp0LcZehfYKIirtwKMBycMMqMA1/Dc9/BlUqIEtXO75mIzMJ3PYCV1Ji+BpoUCk+lTzRfh9c+ptGdyQ==} - engines: {node: '>=12', npm: '>=6'} - mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -620,10 +497,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -631,6 +504,10 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -648,43 +525,26 @@ packages: resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} engines: {node: '>=10'} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - nwsapi@2.2.23: resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - ow@0.28.2: - resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} - engines: {node: '>=12'} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -724,10 +584,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.4.4: - resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} - engines: {node: '>=11.0.0'} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -760,9 +616,9 @@ packages: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tiny-lru@11.4.5: - resolution: {integrity: sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==} - engines: {node: '>=12'} + tar@7.5.13: + resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} + engines: {node: '>=18'} tough-cookie@4.1.4: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} @@ -772,9 +628,6 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -794,13 +647,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-is-frozen@0.1.2: - resolution: {integrity: sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==} - - ua-parser-js@2.0.7: - resolution: {integrity: sha512-CFdHVHr+6YfbktNZegH3qbYvYgC7nRNEUm2tk7nSFXSODUu4tDBpaFpP1jdXBUOKKwapVlWRfTtS8bCPzsQ47w==} - hasBin: true - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -808,22 +654,12 @@ packages: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - vali-date@1.0.0: - resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} - engines: {node: '>=0.10.0'} - w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -864,17 +700,13 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} - xml2js@0.6.2: - resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + snapshots: '@asamuzakjp/css-color@3.2.0': @@ -983,18 +815,14 @@ snapshots: '@esbuild/win32-x64@0.27.0': optional: true - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': + '@isaacs/fs-minipass@4.0.1': dependencies: - '@isaacs/balanced-match': 4.0.1 + minipass: 7.1.2 '@mixmark-io/domino@2.2.0': {} '@mozilla/readability@0.5.0': {} - '@sindresorhus/is@4.6.0': {} - '@types/jsdom@21.1.7': dependencies: '@types/node': 25.0.6 @@ -1011,17 +839,13 @@ snapshots: '@types/turndown@5.0.6': {} - adm-zip@0.5.16: {} - agent-base@7.1.4: {} asynckit@0.4.0: {} base64-js@1.5.1: {} - baseline-browser-mapping@2.9.14: {} - - better-sqlite3@12.6.0: + better-sqlite3@12.8.0: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 @@ -1036,14 +860,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001764 - electron-to-chromium: 1.5.267 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -1054,33 +870,21 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - callsites@3.1.0: {} - - camoufox-js@0.8.5(playwright-core@1.57.0): - dependencies: - adm-zip: 0.5.16 - better-sqlite3: 12.6.0 - commander: 14.0.2 - fingerprint-generator: 2.1.79 - glob: 13.0.0 - impit: 0.7.6 - language-tags: 2.1.0 - maxmind: 5.0.3 - playwright-core: 1.57.0 - progress: 2.0.3 - ua-parser-js: 2.0.7 - xml2js: 0.6.2 - - caniuse-lite@1.0.30001764: {} - chownr@1.1.4: {} + chownr@3.0.0: {} + + cloakbrowser@0.3.22(mmdb-lib@3.0.1)(playwright-core@1.59.1): + dependencies: + tar: 7.5.13 + optionalDependencies: + mmdb-lib: 3.0.1 + playwright-core: 1.59.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - commander@14.0.2: {} - cssstyle@4.6.0: dependencies: '@asamuzakjp/css-color': 3.2.0 @@ -1105,22 +909,14 @@ snapshots: delayed-stream@1.0.0: {} - detect-europe-js@0.1.2: {} - detect-libc@2.1.2: {} - dot-prop@6.0.1: - dependencies: - is-obj: 2.0.0 - dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.267: {} - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -1171,18 +967,10 @@ snapshots: '@esbuild/win32-ia32': 0.27.0 '@esbuild/win32-x64': 0.27.0 - escalade@3.2.0: {} - expand-template@2.0.3: {} file-uri-to-path@1.0.0: {} - fingerprint-generator@2.1.79: - dependencies: - generative-bayesian-network: 2.1.79 - header-generator: 2.1.79 - tslib: 2.8.1 - form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -1198,11 +986,6 @@ snapshots: function-bind@1.1.2: {} - generative-bayesian-network@2.1.79: - dependencies: - adm-zip: 0.5.16 - tslib: 2.8.1 - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -1227,12 +1010,6 @@ snapshots: github-from-package@0.0.0: {} - glob@13.0.0: - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - gopd@1.2.0: {} has-symbols@1.1.0: {} @@ -1245,13 +1022,6 @@ snapshots: dependencies: function-bind: 1.1.2 - header-generator@2.1.79: - dependencies: - browserslist: 4.28.1 - generative-bayesian-network: 2.1.79 - ow: 0.28.2 - tslib: 2.8.1 - html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -1276,51 +1046,12 @@ snapshots: ieee754@1.2.1: {} - impit-darwin-arm64@0.7.6: - optional: true - - impit-darwin-x64@0.7.6: - optional: true - - impit-linux-arm64-gnu@0.7.6: - optional: true - - impit-linux-arm64-musl@0.7.6: - optional: true - - impit-linux-x64-gnu@0.7.6: - optional: true - - impit-linux-x64-musl@0.7.6: - optional: true - - impit-win32-arm64-msvc@0.7.6: - optional: true - - impit-win32-x64-msvc@0.7.6: - optional: true - - impit@0.7.6: - optionalDependencies: - impit-darwin-arm64: 0.7.6 - impit-darwin-x64: 0.7.6 - impit-linux-arm64-gnu: 0.7.6 - impit-linux-arm64-musl: 0.7.6 - impit-linux-x64-gnu: 0.7.6 - impit-linux-x64-musl: 0.7.6 - impit-win32-arm64-msvc: 0.7.6 - impit-win32-x64-msvc: 0.7.6 - inherits@2.0.4: {} ini@1.3.8: {} - is-obj@2.0.0: {} - is-potential-custom-element-name@1.0.1: {} - is-standalone-pwa@0.1.1: {} - jsdom@24.1.3: dependencies: cssstyle: 4.6.0 @@ -1349,25 +1080,10 @@ snapshots: - supports-color - utf-8-validate - language-subtag-registry@0.3.23: {} - - language-tags@2.1.0: - dependencies: - language-subtag-registry: 0.3.23 - - lodash.isequal@4.5.0: {} - lru-cache@10.4.3: {} - lru-cache@11.2.4: {} - math-intrinsics@1.1.0: {} - maxmind@5.0.3: - dependencies: - mmdb-lib: 3.0.1 - tiny-lru: 11.4.5 - mime-db@1.52.0: {} mime-types@2.1.35: @@ -1376,17 +1092,18 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimist@1.2.8: {} minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + mkdirp-classic@0.5.3: {} - mmdb-lib@3.0.1: {} + mmdb-lib@3.0.1: + optional: true ms@2.1.3: {} @@ -1396,34 +1113,17 @@ snapshots: dependencies: semver: 7.7.3 - node-releases@2.0.27: {} - nwsapi@2.2.23: {} once@1.4.0: dependencies: wrappy: 1.0.2 - ow@0.28.2: - dependencies: - '@sindresorhus/is': 4.6.0 - callsites: 3.1.0 - dot-prop: 6.0.1 - lodash.isequal: 4.5.0 - vali-date: 1.0.0 - parse5@7.3.0: dependencies: entities: 6.0.1 - path-scurry@2.0.1: - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - - picocolors@1.1.1: {} - - playwright-core@1.57.0: {} + playwright-core@1.59.1: {} prebuild-install@7.1.3: dependencies: @@ -1440,8 +1140,6 @@ snapshots: tar-fs: 2.1.4 tunnel-agent: 0.6.0 - progress@2.0.3: {} - psl@1.15.0: dependencies: punycode: 2.3.1 @@ -1480,8 +1178,6 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.4: {} - saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -1519,7 +1215,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - tiny-lru@11.4.5: {} + tar@7.5.13: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 tough-cookie@4.1.4: dependencies: @@ -1532,8 +1234,6 @@ snapshots: dependencies: punycode: 2.3.1 - tslib@2.8.1: {} - tsx@4.21.0: dependencies: esbuild: 0.27.0 @@ -1553,24 +1253,10 @@ snapshots: typescript@5.9.3: {} - ua-is-frozen@0.1.2: {} - - ua-parser-js@2.0.7: - dependencies: - detect-europe-js: 0.1.2 - is-standalone-pwa: 0.1.1 - ua-is-frozen: 0.1.2 - undici-types@7.16.0: {} universalify@0.2.0: {} - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - url-parse@1.5.10: dependencies: querystringify: 2.2.0 @@ -1578,8 +1264,6 @@ snapshots: util-deprecate@1.0.2: {} - vali-date@1.0.0: {} - w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -1603,11 +1287,6 @@ snapshots: xml-name-validator@5.0.0: {} - xml2js@0.6.2: - dependencies: - sax: 1.4.4 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - xmlchars@2.2.0: {} + + yallist@5.0.0: {} diff --git a/skills/web-automation/opencode/scripts/reference-source.json b/skills/web-automation/opencode/scripts/reference-source.json new file mode 100644 index 0000000..e84ddeb --- /dev/null +++ b/skills/web-automation/opencode/scripts/reference-source.json @@ -0,0 +1,24 @@ +{ + "referenceRepo": "https://git.fiorinis.com/Home/stef-openclaw-skills", + "referenceCommit": "b9878e938c1055e0284876aeb65157286d95f9d1", + "importedFiles": [ + "auth.ts", + "browse.ts", + "check-install.js", + "extract.js", + "flow.ts", + "package.json", + "scan-local-app.ts", + "scrape.ts", + "test-full.ts", + "test-minimal.ts", + "test-profile.ts" + ], + "excludedReferencePatterns": [ + "*discover.js", + "*photos.js", + "*identifiers.js", + "*.test.mjs", + "domain-specific helper scripts" + ] +} diff --git a/skills/web-automation/opencode/scripts/scan-local-app.ts b/skills/web-automation/opencode/scripts/scan-local-app.ts index cae88e1..6a05b35 100644 --- a/skills/web-automation/opencode/scripts/scan-local-app.ts +++ b/skills/web-automation/opencode/scripts/scan-local-app.ts @@ -1,12 +1,9 @@ -import { writeFileSync } from 'fs'; +#!/usr/bin/env npx tsx + +import { mkdirSync, writeFileSync } from 'fs'; +import { dirname, resolve } from 'path'; import { getPage } from './browse.js'; -const baseUrl = 'http://localhost:3000'; -const username = 'analyst@fhb.local'; -const password = process.env.CAMOUFOX_PASSWORD ?? ''; - -const reportPath = '/Users/stefano.fiorini/Documents/projects/fhb-loan-spreading-pilot-a/docs/plans/2026-01-24-financials-analysis-redesign/web-automation-scan.md'; - type NavResult = { requestedUrl: string; url: string; @@ -15,198 +12,163 @@ type NavResult = { error?: string; }; +type RouteCheck = { + route: string; + result: NavResult; + heading: string | null; +}; + +const DEFAULT_BASE_URL = 'http://localhost:3000'; +const DEFAULT_REPORT_PATH = resolve(process.cwd(), 'scan-local-app.md'); + +function env(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value ? value : undefined; +} + +function getRoutes(baseUrl: string): string[] { + const routeList = env('SCAN_ROUTES'); + if (routeList) { + return routeList + .split(',') + .map((route) => route.trim()) + .filter(Boolean) + .map((route) => new URL(route, baseUrl).toString()); + } + + return [baseUrl]; +} + async function gotoWithStatus(page: any, url: string): Promise { - const resp = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch((e: unknown) => ({ error: e })); - if (resp?.error) { + const response = await page + .goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }) + .catch((error: unknown) => ({ error })); + + if (response?.error) { return { requestedUrl: url, url: page.url(), status: null, title: await page.title().catch(() => ''), - error: String(resp.error), + error: String(response.error), }; } return { requestedUrl: url, url: page.url(), - status: resp ? resp.status() : null, + status: response ? response.status() : null, title: await page.title().catch(() => ''), }; } async function textOrNull(page: any, selector: string): Promise { - const loc = page.locator(selector).first(); + const locator = page.locator(selector).first(); try { - if ((await loc.count()) === 0) return null; - const txt = await loc.textContent(); - return txt ? txt.trim().replace(/\s+/g, ' ') : null; + if ((await locator.count()) === 0) return null; + const value = await locator.textContent(); + return value ? value.trim().replace(/\s+/g, ' ') : null; } catch { return null; } } +async function loginIfConfigured(page: any, baseUrl: string, lines: string[]) { + const loginPath = env('SCAN_LOGIN_PATH'); + const username = env('SCAN_USERNAME') ?? env('CLOAKBROWSER_USERNAME'); + const password = env('SCAN_PASSWORD') ?? env('CLOAKBROWSER_PASSWORD'); + const usernameSelector = env('SCAN_USERNAME_SELECTOR') ?? 'input[type="email"], input[name="email"]'; + const passwordSelector = env('SCAN_PASSWORD_SELECTOR') ?? 'input[type="password"], input[name="password"]'; + const submitSelector = env('SCAN_SUBMIT_SELECTOR') ?? 'button[type="submit"], input[type="submit"]'; + + if (!loginPath) { + lines.push('## Login'); + lines.push('- Skipped: set `SCAN_LOGIN_PATH` to enable login smoke checks.'); + lines.push(''); + return; + } + + const loginUrl = new URL(loginPath, baseUrl).toString(); + lines.push('## Login'); + lines.push(`- Login URL: ${loginUrl}`); + await gotoWithStatus(page, loginUrl); + + if (!username || !password) { + lines.push('- Skipped: set `SCAN_USERNAME`/`SCAN_PASSWORD` or `CLOAKBROWSER_USERNAME`/`CLOAKBROWSER_PASSWORD`.'); + lines.push(''); + return; + } + + await page.locator(usernameSelector).first().fill(username); + await page.locator(passwordSelector).first().fill(password); + await page.locator(submitSelector).first().click(); + await page.waitForTimeout(2500); + + lines.push(`- After submit URL: ${page.url()}`); + lines.push(`- Cookie count: ${(await page.context().cookies()).length}`); + lines.push(''); +} + +async function checkRoutes(page: any, baseUrl: string, lines: string[]) { + const routes = getRoutes(baseUrl); + const routeChecks: RouteCheck[] = []; + + for (const url of routes) { + const result = await gotoWithStatus(page, url); + const heading = await textOrNull(page, 'h1'); + routeChecks.push({ + route: url, + result, + heading, + }); + } + + lines.push('## Route Checks'); + for (const check of routeChecks) { + const relativeUrl = check.route.startsWith(baseUrl) ? check.route.slice(baseUrl.length) || '/' : check.route; + const finalPath = check.result.url.startsWith(baseUrl) + ? check.result.url.slice(baseUrl.length) || '/' + : check.result.url; + const suffix = check.heading ? `, h1="${check.heading}"` : ''; + const errorSuffix = check.result.error ? `, error="${check.result.error}"` : ''; + lines.push( + `- ${relativeUrl} → status ${check.result.status ?? 'ERR'} (final ${finalPath})${suffix}${errorSuffix}` + ); + } + lines.push(''); +} + async function main() { - const { page, browser } = await getPage({ headless: true }); + const baseUrl = env('SCAN_BASE_URL') ?? DEFAULT_BASE_URL; + const reportPath = resolve(env('SCAN_REPORT_PATH') ?? DEFAULT_REPORT_PATH); + const headless = (env('SCAN_HEADLESS') ?? env('CLOAKBROWSER_HEADLESS') ?? 'true') === 'true'; + const { page, browser } = await getPage({ headless }); const lines: string[] = []; lines.push('# Web Automation Scan (local)'); lines.push(''); lines.push(`- Base URL: ${baseUrl}`); lines.push(`- Timestamp: ${new Date().toISOString()}`); + lines.push(`- Headless: ${headless}`); + lines.push(`- Report Path: ${reportPath}`); lines.push(''); try { - lines.push('## Login'); - await gotoWithStatus(page, `${baseUrl}/login`); - await page.locator('input[name="email"]').fill(username); - await page.locator('input[name="password"]').fill(password); - await page.locator('button[type="submit"]').click(); - await page.waitForTimeout(2500); - - const cookies = await page.context().cookies(); - const sessionCookie = cookies.find((c: any) => c.name === 'fhb_session'); - lines.push(`- After submit URL: ${page.url()}`); - lines.push(`- Has session cookie (fhb_session): ${Boolean(sessionCookie)}`); - lines.push(''); - - lines.push('## Demo Case'); - const casesNav = await gotoWithStatus(page, `${baseUrl}/cases`); - lines.push(`- GET /cases → status ${casesNav.status ?? 'ERR'}, final ${casesNav.url}`); - - const envCaseId = process.env.SCAN_CASE_ID?.trim() || null; - let selectedCaseId: string | null = envCaseId; - - if (!selectedCaseId) { - const caseLinks = await page.$$eval('a[href^="/cases/"]', (as) => - as - .map((a) => ({ - href: (a as HTMLAnchorElement).getAttribute('href') || '', - text: (a.textContent || '').trim(), - })) - .filter((x) => x.href.includes('/cases/')) - ); - - const preferredTitles = ['Demo - Strong Borrower', 'Demo - Weak Borrower', 'Demo - Incomplete']; - for (const title of preferredTitles) { - const match = caseLinks.find((l) => l.text.includes(title) && l.href.includes('/cases/')); - const href = match?.href ?? ''; - const m = href.match(/\/cases\/([0-9a-f-]{36})/i); - if (m) { - selectedCaseId = m[1]; - break; - } - } - - if (!selectedCaseId) { - const firstHref = - caseLinks.map((l) => l.href).find((h) => /\/cases\/[0-9a-f-]{36}/i.test(h)) ?? null; - const m = firstHref?.match(/\/cases\/([0-9a-f-]{36})/i) ?? null; - selectedCaseId = m?.[1] ?? null; - } - } - - lines.push(`- Selected caseId: ${selectedCaseId ?? '(none found)'}`); - - if (!selectedCaseId) { - lines.push(''); - lines.push('⚠️ Could not find a demo case link on /cases.'); - writeFileSync(reportPath, lines.join('\n') + '\n', 'utf-8'); - return; - } - - const caseBase = `${baseUrl}/cases/${selectedCaseId}/journey`; - - lines.push(''); - lines.push('## Route Checks'); - - const routesToCheck = [ - `${caseBase}`, - `${caseBase}/financials`, - `${caseBase}/financials/income`, - `${caseBase}/analysis`, - `${caseBase}/analysis/configure`, - `${caseBase}/analysis/ai`, - `${caseBase}/analysis/ai/detail`, - `${caseBase}/spreads`, - ]; - - for (const url of routesToCheck) { - const r = await gotoWithStatus(page, url); - const h1 = await textOrNull(page, 'h1'); - const finalPath = r.url.startsWith(baseUrl) ? r.url.slice(baseUrl.length) : r.url; - lines.push(`- ${url.slice(baseUrl.length)} → status ${r.status ?? 'ERR'} (final ${finalPath})${h1 ? `, h1="${h1}"` : ''}`); - } - - lines.push(''); - lines.push('## Spreadsheet Analysis (UI)'); - await gotoWithStatus(page, `${caseBase}/analysis/configure`); - - const runButton = page.locator('button:has-text("Run Analysis")').first(); - const disabled = await runButton.isDisabled().catch(() => true); - lines.push(`- Run button disabled: ${disabled}`); - - if (!disabled) { - await runButton.click(); - - const resultsWait = page - .waitForURL('**/journey/analysis/results**', { timeout: 180000 }) - .then(() => 'results' as const); - const errorWait = page - .locator('[role="alert"]') - .filter({ hasText: 'Error' }) - .first() - .waitFor({ timeout: 180000 }) - .then(() => 'error' as const); - - const outcome = await Promise.race([resultsWait, errorWait]).catch(() => 'timeout' as const); - - if (outcome === 'results') { - await page.waitForTimeout(1500); - lines.push(`- Results URL: ${page.url().replace(baseUrl, '')}`); - - const downloadHref = await page - .locator('a[href*="/journey/analysis/download"]') - .first() - .getAttribute('href') - .catch(() => null); - - if (downloadHref) { - const dlUrl = downloadHref.startsWith('http') ? downloadHref : `${baseUrl}${downloadHref}`; - const dlResp = await page.goto(dlUrl, { waitUntil: 'commit', timeout: 60000 }).catch(() => null); - lines.push( - `- Download route status: ${dlResp?.status() ?? 'ERR'} (Content-Type: ${dlResp?.headers()?.['content-type'] ?? 'n/a'})` - ); - } else { - lines.push('- Download link not found on results page'); - } - } else if (outcome === 'error') { - const errorText = await page - .locator('[role="alert"]') - .first() - .textContent() - .then((t: string | null) => (t ? t.trim().replace(/\\s+/g, ' ') : null)) - .catch(() => null); - lines.push(`- Stayed on configure page; saw error callout: ${errorText ?? '(unable to read)'}`); - lines.push('- Skipping download check because analysis did not complete.'); - } else { - lines.push('- Timed out waiting for results or error after clicking Run Analysis.'); - } - } else { - lines.push('- Skipped running analysis because Run button was disabled.'); - } - - lines.push(''); + await loginIfConfigured(page, baseUrl, lines); + await checkRoutes(page, baseUrl, lines); lines.push('## Notes'); - lines.push('- This scan avoids scraping financial values; it records route availability and basic headings.'); - - writeFileSync(reportPath, lines.join('\n') + '\n', 'utf-8'); + lines.push('- This generic smoke helper records route availability and top-level headings for a local app.'); + lines.push('- Configure login and route coverage with `SCAN_*` environment variables.'); } finally { await browser.close(); } + + mkdirSync(dirname(reportPath), { recursive: true }); + writeFileSync(reportPath, `${lines.join('\n')}\n`, 'utf-8'); + console.log(`Report written to ${reportPath}`); } -main().catch((err) => { - console.error(err); +main().catch((error) => { + console.error(error); process.exitCode = 1; }); diff --git a/skills/web-automation/opencode/scripts/test-full.ts b/skills/web-automation/opencode/scripts/test-full.ts index 42b30f4..356bbab 100644 --- a/skills/web-automation/opencode/scripts/test-full.ts +++ b/skills/web-automation/opencode/scripts/test-full.ts @@ -1,28 +1,25 @@ -import { Camoufox } from 'camoufox-js'; +import { launchPersistentContext } from 'cloakbrowser'; import { homedir } from 'os'; import { join } from 'path'; import { mkdirSync, existsSync } from 'fs'; async function test() { - const profilePath = join(homedir(), '.camoufox-profile'); + const profilePath = join(homedir(), '.cloakbrowser-profile'); if (!existsSync(profilePath)) { mkdirSync(profilePath, { recursive: true }); } console.log('Profile path:', profilePath); - console.log('Launching with full options...'); + console.log('Launching CloakBrowser with full options...'); - const browser = await Camoufox({ + const browser = await launchPersistentContext({ headless: true, - user_data_dir: profilePath, - // humanize: 1.5, // Test without this first - // geoip: true, // Test without this first - // enable_cache: true, - // block_webrtc: false, + userDataDir: profilePath, + humanize: true, }); console.log('Browser launched'); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); console.log('Page created'); await page.goto('https://github.com', { timeout: 30000 }); diff --git a/skills/web-automation/opencode/scripts/test-minimal.ts b/skills/web-automation/opencode/scripts/test-minimal.ts index 8f2f90a..a5412e7 100644 --- a/skills/web-automation/opencode/scripts/test-minimal.ts +++ b/skills/web-automation/opencode/scripts/test-minimal.ts @@ -1,10 +1,11 @@ -import { Camoufox } from 'camoufox-js'; +import { launch } from 'cloakbrowser'; async function test() { - console.log('Launching Camoufox with minimal config...'); + console.log('Launching CloakBrowser with minimal config...'); - const browser = await Camoufox({ + const browser = await launch({ headless: true, + humanize: true, }); console.log('Browser launched'); diff --git a/skills/web-automation/opencode/scripts/test-profile.ts b/skills/web-automation/opencode/scripts/test-profile.ts index 9afd227..ec59ddd 100644 --- a/skills/web-automation/opencode/scripts/test-profile.ts +++ b/skills/web-automation/opencode/scripts/test-profile.ts @@ -1,24 +1,25 @@ -import { Camoufox } from 'camoufox-js'; +import { launchPersistentContext } from 'cloakbrowser'; import { homedir } from 'os'; import { join } from 'path'; import { mkdirSync, existsSync } from 'fs'; async function test() { - const profilePath = join(homedir(), '.camoufox-profile'); + const profilePath = join(homedir(), '.cloakbrowser-profile'); if (!existsSync(profilePath)) { mkdirSync(profilePath, { recursive: true }); } console.log('Profile path:', profilePath); - console.log('Launching with user_data_dir...'); + console.log('Launching with persistent userDataDir...'); - const browser = await Camoufox({ + const browser = await launchPersistentContext({ headless: true, - user_data_dir: profilePath, + userDataDir: profilePath, + humanize: true, }); console.log('Browser launched'); - const page = await browser.newPage(); + const page = browser.pages()[0] || await browser.newPage(); console.log('Page created'); await page.goto('https://example.com', { timeout: 30000 }); diff --git a/skills/web-automation/opencode/scripts/tmp-extract-firsthorizon-colors.ts b/skills/web-automation/opencode/scripts/tmp-extract-firsthorizon-colors.ts deleted file mode 100644 index 414dfff..0000000 --- a/skills/web-automation/opencode/scripts/tmp-extract-firsthorizon-colors.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { getPage } from './browse.js'; - -type Extracted = { - title: string; - url: string; - colorVars: Array<[string, string]>; - samples: Record; -}; - -function isColorValue(value: string) { - return /#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i.test(value) || /\brgb\(|\bhsl\(/i.test(value); -} - -async function main() { - const url = process.argv[2] ?? 'https://www.firsthorizon.com'; - - const { page, browser } = await getPage({ headless: true }); - try { - await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }); - await page.waitForTimeout(5000); - - const data = await page.evaluate(`(() => { - const rootStyles = getComputedStyle(document.documentElement); - const vars = {}; - for (let i = 0; i < rootStyles.length; i++) { - const prop = rootStyles[i]; - if (prop && prop.startsWith('--')) { - vars[prop] = rootStyles.getPropertyValue(prop).trim(); - } - } - - const pick = (selector) => { - const el = document.querySelector(selector); - if (!el) return null; - const cs = getComputedStyle(el); - return { - background: cs.backgroundColor, - color: cs.color, - border: cs.borderColor, - }; - }; - - return { - title: document.title, - url: location.href, - vars, - samples: { - body: pick('body'), - header: pick('header'), - nav: pick('nav'), - primaryButton: pick('button, [role="button"], a[role="button"], a.button, .button'), - link: pick('a'), - }, - }; - })()`); - - const entries = Object.entries(data.vars) as Array<[string, string]>; - const colorVars = entries - .filter(([, v]) => v && isColorValue(v)) - .sort((a, b) => a[0].localeCompare(b[0])); - - const out: Extracted = { - title: data.title, - url: data.url, - colorVars, - samples: data.samples, - }; - - process.stdout.write(JSON.stringify(out, null, 2)); - } finally { - await browser.close(); - } -} - -main().catch((error) => { - console.error(error); - process.exit(1); -});