Initial commit
This commit is contained in:
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