Initial commit
This commit is contained in:
123
skills/browser-control/SKILL.md
Normal file
123
skills/browser-control/SKILL.md
Normal 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
|
||||
180
skills/browser-control/bun.lock
Normal file
180
skills/browser-control/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
15
skills/browser-control/package.json
Normal file
15
skills/browser-control/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
75
skills/browser-control/scripts/capture.js
Normal file
75
skills/browser-control/scripts/capture.js
Normal 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();
|
||||
93
skills/browser-control/scripts/extract.js
Normal file
93
skills/browser-control/scripts/extract.js
Normal 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();
|
||||
96
skills/browser-control/scripts/interact.js
Normal file
96
skills/browser-control/scripts/interact.js
Normal 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();
|
||||
50
skills/browser-control/scripts/launch-chrome.sh
Executable file
50
skills/browser-control/scripts/launch-chrome.sh
Executable 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
|
||||
113
skills/browser-control/scripts/navigate.js
Normal file
113
skills/browser-control/scripts/navigate.js
Normal 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();
|
||||
110
skills/browser-control/scripts/screenshot.js
Normal file
110
skills/browser-control/scripts/screenshot.js
Normal 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();
|
||||
129
skills/browser-control/scripts/search.js
Normal file
129
skills/browser-control/scripts/search.js
Normal 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();
|
||||
Reference in New Issue
Block a user