Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:35:59 +08:00
commit 90883a4d25
287 changed files with 75058 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
---
name: browser-control
description: Full browser control for authenticated web interactions using Playwright scripts
triggers:
- "check availability"
- "search for"
- "log into"
- "browse to"
- "look up prices"
- "check points"
- "find deals"
- "scrape"
- "get current price"
- "check hotel"
- "check flight"
allowed-tools: Bash, Read
version: 0.1.0
---
# Browser Control Skill
Full browser automation for travel research requiring authentication or complex interactions.
## When to Activate
Use this skill when you need to:
- Access authenticated pages (Marriott, Alaska Airlines accounts)
- Check real-time availability and prices
- Scrape forum threads (FlyerTalk, Reddit)
- Interact with JavaScript-heavy travel sites
- Fill forms or perform searches on websites
## Architecture
**Script-based approach** - No MCP overhead. Scripts load only when needed.
### Prerequisites
1. **Geoffrey Chrome Profile** must be running with remote debugging:
```bash
./scripts/launch-chrome.sh
```
2. **Profile must have logins saved** for:
- Marriott Bonvoy
- Alaska Airlines Mileage Plan
- FlyerTalk
- TripAdvisor
- Reddit
## Available Scripts
All scripts are in `./scripts/` and use Playwright connecting via CDP.
| Script | Purpose | Usage |
|--------|---------|-------|
| `launch-chrome.sh` | Start Geoffrey Chrome profile | `./scripts/launch-chrome.sh` |
| `navigate.js` | Navigate to URL and get page content | `bun scripts/navigate.js <url>` |
| `screenshot.js` | Take screenshot of page | `bun scripts/screenshot.js <url> [output] [--full]` |
| `extract.js` | Extract text/data from page | `bun scripts/extract.js <url> <selector> [--all]` |
| `interact.js` | Click, type, select on page | `bun scripts/interact.js <url> <action> <selector> [value]` |
| `search.js` | Search travel sites | `bun scripts/search.js <site> <query>` |
## Usage Examples
### Check Marriott Points Availability
```bash
# Navigate to Marriott search
bun scripts/navigate.js "https://www.marriott.com/search/default.mi"
# Or use the search script
bun scripts/search.js marriott "Westin Rusutsu February 2026"
```
### Get FlyerTalk Thread Content
```bash
bun scripts/extract.js "https://www.flyertalk.com/forum/thread-url" ".post-content"
```
### Screenshot Hotel Page
```bash
bun scripts/screenshot.js "https://www.marriott.com/hotels/travel/ctswi-the-westin-rusutsu-resort/" rusutsu.png
```
## Connection Details
Scripts connect to Chrome via Chrome DevTools Protocol (CDP):
- **URL**: `http://127.0.0.1:9222`
- **Profile**: `~/.chrome-geoffrey`
## Error Handling
If scripts fail to connect:
1. Ensure Chrome is running with `./scripts/launch-chrome.sh`
2. Check port 9222 is not in use: `lsof -i :9222`
3. Kill existing Chrome debugger: `pkill -f "remote-debugging-port"`
## Output Format
All scripts return JSON:
```json
{
"success": true,
"url": "https://example.com",
"title": "Page Title",
"content": "Extracted content or action result",
"timestamp": "2025-11-22T..."
}
```
## Limitations
- Requires Geoffrey Chrome profile to be running
- Cannot bypass CAPTCHAs (uses real browser fingerprint to avoid most)
- Heavy sites may be slow
- Some sites block automation despite real browser
## Future Enhancements
- Add cookie/session export for headless runs
- 1Password CLI integration for credential rotation
- Parallel page operations
- Browser-Use (Python) for complex visual tasks

View File

@@ -0,0 +1,180 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "browser-control",
"dependencies": {
"puppeteer-core": "^23.0.0",
},
},
},
"packages": {
"@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="],
"@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="],
"b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="],
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
"bare-fs": ["bare-fs@4.5.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg=="],
"bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="],
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
"bare-stream": ["bare-stream@2.7.0", "", { "dependencies": { "streamx": "^2.21.0" }, "peerDependencies": { "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-buffer", "bare-events"] }, "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A=="],
"bare-url": ["bare-url@2.3.2", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"chromium-bidi": ["chromium-bidi@0.11.0", "", { "dependencies": { "mitt": "3.0.1", "zod": "3.23.8" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="],
"devtools-protocol": ["devtools-protocol@0.0.1367902", "", {}, "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="],
"pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"puppeteer-core": ["puppeteer-core@23.11.1", "", { "dependencies": { "@puppeteer/browsers": "2.6.1", "chromium-bidi": "0.11.0", "debug": "^4.4.0", "devtools-protocol": "0.0.1367902", "typed-query-selector": "^2.12.0", "ws": "^8.18.0" } }, "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="],
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"tar-fs": ["tar-fs@3.1.1", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typed-query-selector": ["typed-query-selector@2.12.0", "", {}, "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="],
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"zod": ["zod@3.23.8", "", {}, "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g=="],
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "browser-control",
"version": "0.1.0",
"description": "Browser control scripts for Geoffrey using Playwright",
"type": "module",
"scripts": {
"launch": "./scripts/launch-chrome.sh",
"navigate": "bun scripts/navigate.js",
"screenshot": "bun scripts/screenshot.js",
"extract": "bun scripts/extract.js"
},
"dependencies": {
"puppeteer-core": "^23.0.0"
}
}

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env bun
/**
* Capture current page state (no navigation)
*
* Usage: bun capture.js [output.png]
*/
import puppeteer from 'puppeteer-core';
import path from 'path';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
async function capture(outputPath) {
let browser;
try {
browser = await puppeteer.connect({
browserURL: CDP_ENDPOINT,
defaultViewport: null
});
const pages = await browser.pages();
if (pages.length === 0) {
throw new Error('No pages open in browser');
}
const page = pages[0];
const url = page.url();
const title = await page.title();
// Take screenshot if output path provided
if (outputPath) {
await page.screenshot({ path: outputPath });
}
// Get content
const content = await page.evaluate(() => {
const main = document.querySelector('main') ||
document.querySelector('article') ||
document.querySelector('#content') ||
document.body;
return main.innerText.substring(0, 10000);
});
return {
success: true,
url,
title,
content,
screenshot: outputPath ? path.resolve(outputPath) : null,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
error: error.message,
timestamp: new Date().toISOString()
};
} finally {
if (browser) {
browser.disconnect();
}
}
}
async function main() {
const outputPath = process.argv[2] || null;
const result = await capture(outputPath);
console.log(JSON.stringify(result, null, 2));
if (!result.success) process.exit(1);
}
main();

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env bun
/**
* Extract content from webpage using CSS selectors
*
* Usage: bun extract.js <url> <selector> [--all] [--attr <attribute>]
*/
import puppeteer from 'puppeteer-core';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
async function extract(url, selector, options = {}) {
let browser;
try {
browser = await puppeteer.connect({
browserURL: CDP_ENDPOINT,
defaultViewport: null
});
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await page.waitForSelector(selector, { timeout: 10000 });
let extracted;
if (options.all) {
extracted = await page.$$eval(selector, (elements, attr) => {
return elements.map(el => attr ? el.getAttribute(attr) : el.innerText.trim());
}, options.attr);
} else {
extracted = await page.$eval(selector, (el, attr) => {
return attr ? el.getAttribute(attr) : el.innerText.trim();
}, options.attr);
}
const title = await page.title();
await page.close();
return {
success: true,
url,
title,
selector,
extracted,
count: options.all ? extracted.length : 1,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
url,
selector,
error: error.message,
timestamp: new Date().toISOString()
};
} finally {
if (browser) browser.disconnect();
}
}
async function main() {
const args = process.argv.slice(2);
const url = args[0];
const selector = args[1];
if (!url || !selector) {
console.error(JSON.stringify({
error: 'Missing arguments',
usage: 'bun extract.js <url> <selector> [--all] [--attr <name>]'
}));
process.exit(1);
}
const options = {};
for (let i = 2; i < args.length; i++) {
if (args[i] === '--all') options.all = true;
else if (args[i] === '--attr') options.attr = args[++i];
}
const result = await extract(url, selector, options);
console.log(JSON.stringify(result, null, 2));
if (!result.success) process.exit(1);
}
main();

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bun
/**
* Interact with webpage elements (click, type, select)
*
* Usage: bun interact.js <url> <action> <selector> [value]
*/
import puppeteer from 'puppeteer-core';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
async function interact(url, action, selector, value = null) {
let browser;
try {
browser = await puppeteer.connect({
browserURL: CDP_ENDPOINT,
defaultViewport: null
});
const page = await browser.newPage();
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
await page.waitForSelector(selector, { timeout: 10000 });
let result;
switch (action) {
case 'click':
await page.click(selector);
result = 'clicked';
break;
case 'type':
await page.type(selector, value);
result = `typed: ${value}`;
break;
case 'select':
await page.select(selector, value);
result = `selected: ${value}`;
break;
default:
throw new Error(`Unknown action: ${action}`);
}
await page.waitForTimeout(1000);
const title = await page.title();
const finalUrl = page.url();
await page.close();
return {
success: true,
url: finalUrl,
title,
action,
selector,
result,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
url,
action,
selector,
error: error.message,
timestamp: new Date().toISOString()
};
} finally {
if (browser) browser.disconnect();
}
}
async function main() {
const args = process.argv.slice(2);
const [url, action, selector, value] = args;
if (!url || !action || !selector) {
console.error(JSON.stringify({
error: 'Missing arguments',
usage: 'bun interact.js <url> <action> <selector> [value]'
}));
process.exit(1);
}
const result = await interact(url, action, selector, value);
console.log(JSON.stringify(result, null, 2));
if (!result.success) process.exit(1);
}
main();

View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Launch Geoffrey Chrome Profile with Remote Debugging
#
# This starts a dedicated Chrome profile for Geoffrey's browser automation.
# The profile persists logins, cookies, and extensions between sessions.
#
# Usage: ./launch-chrome.sh [--headless]
PROFILE_DIR="$HOME/.brave-geoffrey"
PORT=9222
# Check if Chrome is already running with debugging
if lsof -i :$PORT > /dev/null 2>&1; then
echo '{"status": "already_running", "port": '$PORT', "profile": "'$PROFILE_DIR'"}'
exit 0
fi
# Create profile directory if it doesn't exist
if [ ! -d "$PROFILE_DIR" ]; then
mkdir -p "$PROFILE_DIR"
echo "Created new Geoffrey Chrome profile at $PROFILE_DIR"
echo "Please log into your accounts (Marriott, Alaska, etc.) on first run."
fi
# Check for headless flag
HEADLESS=""
if [ "$1" = "--headless" ]; then
HEADLESS="--headless=new"
fi
# Launch Brave Nightly with remote debugging (bypasses district MDM)
/Applications/Brave\ Browser\ Nightly.app/Contents/MacOS/Brave\ Browser\ Nightly \
--remote-debugging-port=$PORT \
--user-data-dir="$PROFILE_DIR" \
$HEADLESS \
--no-first-run \
--no-default-browser-check \
&
# Wait for Chrome to start
sleep 2
# Verify it's running
if lsof -i :$PORT > /dev/null 2>&1; then
echo '{"status": "started", "port": '$PORT', "profile": "'$PROFILE_DIR'", "headless": "'$HEADLESS'"}'
else
echo '{"status": "failed", "error": "Chrome did not start on port '$PORT'"}'
exit 1
fi

View File

@@ -0,0 +1,113 @@
#!/usr/bin/env bun
/**
* Navigate to URL and return page content
*
* Connects to Geoffrey's browser profile via CDP and navigates to the specified URL.
* Returns page title, URL, and text content.
*
* Usage: bun navigate.js <url> [--wait <selector>]
*
* Examples:
* bun navigate.js https://www.marriott.com
* bun navigate.js https://flyertalk.com/forum --wait ".post-content"
*/
import puppeteer from 'puppeteer-core';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
async function navigate(url, options = {}) {
let browser;
try {
// Connect to existing browser instance via CDP
browser = await puppeteer.connect({
browserURL: CDP_ENDPOINT,
defaultViewport: null
});
// Always create a new page to avoid interfering with user's tabs
const page = await browser.newPage();
// Navigate to URL
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
// Wait for specific selector if provided
if (options.waitFor) {
await page.waitForSelector(options.waitFor, { timeout: 10000 });
}
// Get page info
const title = await page.title();
const content = await page.evaluate(() => {
// Get main content, avoiding nav/footer
const main = document.querySelector('main') ||
document.querySelector('article') ||
document.querySelector('#content') ||
document.body;
return main.innerText.substring(0, 10000); // Limit content size
});
// Get current URL (may have redirected)
const finalUrl = page.url();
return {
success: true,
url: finalUrl,
title,
content,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
url,
error: error.message,
hint: error.message.includes('connect') || error.message.includes('ECONNREFUSED')
? 'Is browser running? Start with: ./scripts/launch-chrome.sh'
: null,
timestamp: new Date().toISOString()
};
} finally {
// Disconnect (don't close - we want browser to stay open)
if (browser) {
browser.disconnect();
}
}
}
// CLI interface
async function main() {
const args = process.argv.slice(2);
const url = args[0];
if (!url) {
console.error(JSON.stringify({
error: 'Missing URL',
usage: 'bun navigate.js <url> [--wait <selector>]'
}));
process.exit(1);
}
// Parse options
const options = {};
for (let i = 1; i < args.length; i++) {
if (args[i] === '--wait') {
options.waitFor = args[++i];
}
}
const result = await navigate(url, options);
console.log(JSON.stringify(result, null, 2));
if (!result.success) {
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bun
/**
* Take screenshot of a webpage
*
* Usage: bun screenshot.js <url> [output.png] [--full]
*
* Options:
* --full Capture full page (not just viewport)
*
* Examples:
* bun screenshot.js https://www.marriott.com
* bun screenshot.js https://www.marriott.com hotel.png --full
*/
import puppeteer from 'puppeteer-core';
import path from 'path';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
async function screenshot(url, outputPath, options = {}) {
let browser;
try {
browser = await puppeteer.connect({
browserURL: CDP_ENDPOINT,
defaultViewport: null
});
const pages = await browser.pages();
const page = pages.length > 0 ? pages[0] : await browser.newPage();
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 30000
});
// Default output path
if (!outputPath) {
const urlObj = new URL(url);
outputPath = `screenshot-${urlObj.hostname}-${Date.now()}.png`;
}
// Take screenshot
await page.screenshot({
path: outputPath,
fullPage: options.fullPage || false
});
const title = await page.title();
return {
success: true,
url,
title,
screenshot: path.resolve(outputPath),
fullPage: options.fullPage || false,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
url,
error: error.message,
hint: error.message.includes('connect') || error.message.includes('ECONNREFUSED')
? 'Is browser running? Start with: ./scripts/launch-chrome.sh'
: null,
timestamp: new Date().toISOString()
};
} finally {
if (browser) {
browser.disconnect();
}
}
}
async function main() {
const args = process.argv.slice(2);
const url = args[0];
if (!url) {
console.error(JSON.stringify({
error: 'Missing URL',
usage: 'bun screenshot.js <url> [output.png] [--full]'
}));
process.exit(1);
}
// Parse args
let outputPath = null;
const options = {};
for (let i = 1; i < args.length; i++) {
if (args[i] === '--full') {
options.fullPage = true;
} else if (!args[i].startsWith('--')) {
outputPath = args[i];
}
}
const result = await screenshot(url, outputPath, options);
console.log(JSON.stringify(result, null, 2));
if (!result.success) {
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bun
/**
* Perform searches on common travel sites
*
* Usage: bun search.js <site> <query>
*/
import puppeteer from 'puppeteer-core';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
const SITES = {
marriott: {
url: (q) => `https://www.marriott.com/search/default.mi?keywords=${encodeURIComponent(q)}`,
resultSelector: '.property-card, .l-container'
},
alaska: {
url: 'https://www.alaskaair.com/',
resultSelector: '.search-results'
},
flyertalk: {
url: (q) => `https://www.flyertalk.com/forum/search.php?do=process&query=${encodeURIComponent(q)}`,
resultSelector: '.searchresult, .threadbit'
},
tripadvisor: {
url: (q) => `https://www.tripadvisor.com/Search?q=${encodeURIComponent(q)}`,
resultSelector: '[data-automation="searchResult"]'
},
reddit: {
url: (q) => `https://www.reddit.com/search/?q=${encodeURIComponent(q)}`,
resultSelector: '[data-testid="post-container"]'
},
google: {
url: (q) => `https://www.google.com/search?q=${encodeURIComponent(q)}`,
resultSelector: '.g'
}
};
async function search(siteName, query) {
let browser;
try {
const site = SITES[siteName.toLowerCase()];
if (!site) {
return {
success: false,
error: `Unknown site: ${siteName}`,
availableSites: Object.keys(SITES)
};
}
browser = await puppeteer.connect({
browserURL: CDP_ENDPOINT,
defaultViewport: null
});
const page = await browser.newPage();
const url = typeof site.url === 'function' ? site.url(query) : site.url;
await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: 30000
});
let results = [];
try {
await page.waitForSelector(site.resultSelector, { timeout: 10000 });
results = await page.$$eval(site.resultSelector, (elements) => {
return elements.slice(0, 10).map(el => {
const link = el.querySelector('a');
return {
text: el.innerText.substring(0, 500).trim(),
url: link ? link.href : null
};
});
});
} catch (e) {
// No results found
}
const title = await page.title();
const finalUrl = page.url();
await page.close();
return {
success: true,
site: siteName,
query,
url: finalUrl,
title,
resultCount: results.length,
results,
timestamp: new Date().toISOString()
};
} catch (error) {
return {
success: false,
site: siteName,
query,
error: error.message,
timestamp: new Date().toISOString()
};
} finally {
if (browser) browser.disconnect();
}
}
async function main() {
const args = process.argv.slice(2);
const site = args[0];
const query = args.slice(1).join(' ');
if (!site || !query) {
console.error(JSON.stringify({
error: 'Missing arguments',
usage: 'bun search.js <site> <query>',
availableSites: Object.keys(SITES)
}));
process.exit(1);
}
const result = await search(site, query);
console.log(JSON.stringify(result, null, 2));
if (!result.success) process.exit(1);
}
main();