diff --git a/.github/workflows/readme-template-index.yml b/.github/workflows/readme-template-index.yml new file mode 100644 index 00000000..5c285543 --- /dev/null +++ b/.github/workflows/readme-template-index.yml @@ -0,0 +1,45 @@ +name: README Template Index + +on: + pull_request: + paths: + - "README.md" + - "go/**" + - "python/**" + - "typescript/**" + - "scripts/check-readme-template-index.mjs" + - ".github/workflows/readme-template-index.yml" + push: + branches: + - dev + paths: + - "README.md" + - "go/**" + - "python/**" + - "typescript/**" + - "scripts/check-readme-template-index.mjs" + - ".github/workflows/readme-template-index.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + validate-readme-template-index: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 20.x + + - name: Validate README template index + run: node scripts/check-readme-template-index.mjs diff --git a/README.md b/README.md index 56fc549f..51ff915f 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,73 @@ # Stagehand + Browserbase Templates -A comprehensive collection of ready-to-use automation templates demonstrating the power of Stagehand and Browserbase for web automation, data extraction, and AI-powered browser interactions. +Ready-to-use automation templates for Stagehand and Browserbase. Each template has its own README with setup instructions. > All templates also live on [browserbase.com/templates](https://www.browserbase.com/templates) -## 🚀 Quick Start +## All Templates + +| Template | TS | PY | GO | Description | +| -------------------------------- | ------------------------------------------------- | --------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| agent-with-human-in-loop | [TS](typescript/agent-with-human-in-loop) | - | - | Build an AI agent that can pause and ask a human for input mid-task | +| amazon-global-price-comparison | [TS](typescript/amazon-global-price-comparison) | [PY](python/amazon-global-price-comparison) | - | Compare Amazon product prices across multiple countries using geolocation proxies | +| amazon-product-scraping | [TS](typescript/amazon-product-scraping) | [PY](python/amazon-product-scraping) | - | Scrape the first 3 Amazon search results for a given query and return structured product data | +| basic-caching | [TS](typescript/basic-caching) | [PY](python/basic-caching) | - | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | +| basic-recaptcha | [TS](typescript/basic-recaptcha) | [PY](python/basic-recaptcha) | - | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | +| browser-agent-demo | [TS](typescript/browser-agent-demo) | - | - | Browser agent that searches the web, fetches page content, and autonomously extracts information | +| browserbase-reducto | [TS](typescript/browserbase-reducto) | [PY](python/browserbase-reducto) | - | Download financial PDFs from websites and extract structured data using AI-powered document parsing | +| business-lookup | [TS](typescript/business-lookup) | [PY](python/business-lookup) | - | Automate business registry searches using an autonomous AI agent with computer-use capabilities | +| cartesia-form-filling | - | [PY](python/cartesia-form-filling) | - | Voice agent that conducts phone questionnaires while automatically filling out web forms | +| cerebras-docs-checker | - | [PY](python/cerebras-docs-checker) | - | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase | +| company-address-finder | [TS](typescript/company-address-finder) | [PY](python/company-address-finder) | - | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | +| company-value-prop-generator | [TS](typescript/company-value-prop-generator) | [PY](python/company-value-prop-generator) | - | Extract and format website value propositions into concise one-liners for email personalization | +| context | [TS](typescript/context) | [PY](python/context) | - | Persistent authentication using Browserbase contexts that survive across sessions | +| council-events | [TS](typescript/council-events) | [PY](python/council-events) | - | Automate event information extraction from Philadelphia Council | +| download-financial-statements | [TS](typescript/download-financial-statements) | [PY](python/download-financial-statements) | - | Download Apple's quarterly financial statements (PDFs) from their investor relations site | +| dynamic-form-filling | [TS](typescript/dynamic-form-filling) | - | - | Intelligent form filling using a Stagehand AI agent that understands form context and uses semantic matching | +| exa-browserbase | [TS](typescript/exa-browserbase) | [PY](python/exa-browserbase) | - | Automate job applications with AI that writes smart, tailored responses for each role | +| extend-browserbase | [TS](typescript/extend-browserbase) | [PY](python/extend-browserbase) | - | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | +| form-filling | [TS](typescript/form-filling) | [PY](python/form-filling) | - | Automate form filling with Stagehand and Browserbase | +| gemini-3-flash | [TS](typescript/gemini-3-flash) | - | - | Autonomous web browsing using Google's Gemini 3 Flash with Stagehand and Browserbase | +| gemini-cua | [TS](typescript/gemini-cua) | [PY](python/gemini-cua) | - | Autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase | +| getting-started-with-browserbase | [TS](typescript/getting-started-with-browserbase) | [PY](python/getting-started-with-browserbase) | - | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | +| gift-finder | [TS](typescript/gift-finder) | [PY](python/gift-finder) | - | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | +| google-trends | [TS](typescript/google-trends) | [PY](python/google-trends) | - | Extract trending search keywords from Google Trends for any country with structured JSON output | +| hackernews | - | - | [GO](go/hackernews) | Demonstrate Stagehand's core browser automation features through a complete Hacker News workflow | +| image-url-download | [TS](typescript/image-url-download) | [PY](python/image-url-download) | - | Extract all image URLs from a page and download each image through the browser's direct connection | +| job-application | [TS](typescript/job-application) | [PY](python/job-application) | - | Automate job applications by discovering job listings and submitting applications | +| license-verification | [TS](typescript/license-verification) | [PY](python/license-verification) | - | Extract structured, validated data from websites using Stagehand + Zod | +| manual-mfa-with-contexts | [TS](typescript/manual-mfa-with-contexts) | [PY](python/manual-mfa-with-contexts) | - | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | +| mfa-handling | [TS](typescript/mfa-handling) | [PY](python/mfa-handling) | - | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | +| microsoft-cua | [TS](typescript/microsoft-cua) | - | - | Autonomous web browsing using Microsoft's Computer Use Agent with Stagehand and Browserbase | +| nurse-verification | [TS](typescript/nurse-verification) | [PY](python/nurse-verification) | - | Automate verification of nurse licenses by filling forms and extracting structured results | +| pickleball | [TS](typescript/pickleball) | [PY](python/pickleball) | - | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | +| playwright | [TS](typescript/playwright) | [PY](python/playwright) | - | Raw Playwright usage with Browserbase (no Stagehand) | +| playwright-mfa-handling | [TS](typescript/playwright-mfa-handling) | [PY](python/playwright-mfa-handling) | - | Automate MFA completion using TOTP with raw Playwright and Browserbase | +| polymarket-research | [TS](typescript/polymarket-research) | [PY](python/polymarket-research) | - | Automate market research on prediction markets using Stagehand | +| proxies | [TS](typescript/proxies) | [PY](python/proxies) | - | Demonstrate different proxy configurations with Browserbase sessions | +| proxies-weather | [TS](typescript/proxies-weather) | [PY](python/proxies-weather) | - | Geolocation proxies fetching location-specific weather data from multiple cities | +| puppeteer | [TS](typescript/puppeteer) | - | - | Raw Puppeteer usage with Browserbase | +| sec-filing-research | [TS](typescript/sec-filing-research) | [PY](python/sec-filing-research) | - | Search SEC EDGAR for a company and extract recent filing metadata | +| selenium | [TS](typescript/selenium) | [PY](python/selenium) | - | Raw Selenium usage with Browserbase | +| smart-fetch-scraper | [TS](typescript/smart-fetch-scraper) | [PY](python/smart-fetch-scraper) | - | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | +| website-link-tester | [TS](typescript/website-link-tester) | [PY](python/website-link-tester) | - | Crawl a website's homepage, collect all links, and verify each link loads successfully | -1. **Choose your language**: TypeScript or Python -2. **Browse available templates** in the respective language folder -3. **Read the template's README** for detailed setup instructions and use cases -4. **Start automating!** - -## 🔧 Getting Started +## Model Gateway -1. **Choose a template** from the TypeScript or Python folders -2. **Read the template's README** for specific setup instructions -3. **Set up your environment** with the required API keys and dependencies -4. **Run the template** and start automating! +Templates use the Model Gateway to route LLM requests -- you only need your `BROWSERBASE_API_KEY`. No separate OpenAI, Anthropic, or Google API keys required. Supported models include OpenAI, Anthropic, and Google (Gemini). -> **💡 Pro Tip**: Each template's README contains detailed installation steps, environment variable requirements, and troubleshooting guides specific to that template. +> **Note**: CUA (Computer Use Agent) models are not yet supported through the Model Gateway. Templates using CUA models still require a separate model provider API key. -## Model Gateway +## Getting Started -Templates use the Model Gateway to route LLM requests — you only need your `BROWSERBASE_API_KEY`. No separate OpenAI, Anthropic, or Google API keys required. Supported models include OpenAI, Anthropic, and Google (Gemini). +1. **Choose a template** from the table above +2. **Read the template's README** for specific setup instructions +3. **Set up your environment** with the required API keys and dependencies +4. **Run the template** and start automating -> **Note**: CUA (Computer Use Agent) models are not yet supported through the Model Gateway. Templates using CUA models still require a separate model provider API key. +Each template's README contains detailed installation steps, environment variable requirements, and troubleshooting guides. -## 📚 Resources +## Resources ### Documentation @@ -35,18 +76,11 @@ Templates use the Model Gateway to route LLM requests — you only need your `BR ### Support -- **Community**: Join our Discord community - **Discord**: http://stagehand.dev/discord - **Email Support**: support@browserbase.com - **GitHub Issues**: Report bugs and request features -### Examples & Tutorials - -- **Getting Started Guide**: Learn the basics of Stagehand -- **Advanced Patterns**: Complex automation workflows -- **Best Practices**: Tips for reliable automation - -## 🤝 Contributing +## Contributing We welcome contributions! Here's how you can help: @@ -62,10 +96,6 @@ We welcome contributions! Here's how you can help: - Add proper error handling and logging - Test templates thoroughly before submitting -## 📄 License +## License This project is licensed under the MIT License - see the LICENSE file for details. - ---- - -**Ready to start automating?** Pick a template and follow its README to get started! 🚀 diff --git a/package.json b/package.json index 45ed7ac4..2be8b400 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "check:readme-template-index": "node scripts/check-readme-template-index.mjs", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "lint": "eslint \"**/*.{ts,tsx,js,jsx}\"", @@ -13,7 +14,7 @@ "lint:python:fix": "uvx ruff check --fix python/", "format:python": "uvx ruff format python/", "format:python:check": "uvx ruff format --check python/", - "check": "npm run format:check && npm run lint && npm run lint:python && npm run format:python:check", + "check": "npm run check:readme-template-index && npm run format:check && npm run lint && npm run lint:python && npm run format:python:check", "prepare": "husky" }, "devDependencies": { diff --git a/scripts/check-readme-template-index.mjs b/scripts/check-readme-template-index.mjs new file mode 100644 index 00000000..c7207ef0 --- /dev/null +++ b/scripts/check-readme-template-index.mjs @@ -0,0 +1,301 @@ +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +const REPO_ROOT = process.cwd(); +const README_PATH = path.join(REPO_ROOT, "README.md"); +const TEMPLATE_ROOTS = { + typescript: "TS", + python: "PY", + go: "GO", +}; +const LANGUAGE_COLUMNS = ["TS", "PY", "GO"]; +const LANGUAGE_TO_ROOT = Object.fromEntries( + Object.entries(TEMPLATE_ROOTS).map(([root, language]) => [language, root]), +); +const SECTION_PATTERN = /^## All Templates\r?\n\r?\n([\s\S]*?)(?=^##\s)/m; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function getTrackedTemplatePaths() { + try { + const gitOutput = execFileSync( + "git", + ["ls-files", "-co", "--exclude-standard", "--", ...Object.keys(TEMPLATE_ROOTS)], + { + cwd: REPO_ROOT, + encoding: "utf8", + }, + ); + + return new Set( + gitOutput + .split(/\r?\n/) + .filter(Boolean) + .map((filePath) => { + const [root, templateName] = filePath.split("/"); + if (!TEMPLATE_ROOTS[root] || !templateName) { + return null; + } + + return `${root}/${templateName}`; + }) + .filter(Boolean), + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + fail(`Could not determine tracked template paths from git:\n${message}`); + } +} + +function getExpectedRows() { + const rows = new Map(); + + for (const templatePath of [...getTrackedTemplatePaths()].sort()) { + const [root, name] = templatePath.split("/"); + const language = TEMPLATE_ROOTS[root]; + const row = rows.get(name) ?? { name, languages: {} }; + row.languages[language] = { label: language, path: templatePath }; + rows.set(name, row); + } + + return [...rows.values()].sort((left, right) => left.name.localeCompare(right.name)); +} + +function getReadmeSection(readmeContents) { + const match = readmeContents.match(SECTION_PATTERN); + if (!match) { + fail("Could not find the `## All Templates` section in README.md."); + } + + return match[1]; +} + +function parseLanguageCell(cell, expectedLanguage, rowName, line) { + if (cell === "-") { + return null; + } + + const match = cell.match(/^\[([A-Z]{2})\]\(([^)]+)\)$/); + if (!match) { + fail( + `Invalid ${expectedLanguage} cell format for template \`${rowName}\` in README.md:\n${line}`, + ); + } + + const [, label, templatePath] = match; + return { label, path: templatePath }; +} + +function parseReadmeRows(section) { + const lines = section + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.startsWith("|")); + + if (lines.length < 3) { + fail("README template table is missing rows."); + } + + const dataLines = lines.slice(2); + + return dataLines.map((line) => { + const cells = line + .split("|") + .slice(1, -1) + .map((cell) => cell.trim()); + + if (cells.length !== 5) { + fail(`Invalid template row format in README.md:\n${line}`); + } + + const [name, tsCell, pyCell, goCell, description] = cells; + + return { + name, + languages: { + TS: parseLanguageCell(tsCell, "TS", name, line), + PY: parseLanguageCell(pyCell, "PY", name, line), + GO: parseLanguageCell(goCell, "GO", name, line), + }, + description: description.trim(), + }; + }); +} + +function findDuplicateNames(rows) { + const counts = new Map(); + + for (const row of rows) { + counts.set(row.name, (counts.get(row.name) ?? 0) + 1); + } + + return [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => name); +} + +function findDuplicatePaths(rows) { + const counts = new Map(); + + for (const row of rows) { + for (const language of LANGUAGE_COLUMNS) { + const cell = row.languages[language]; + if (!cell) { + continue; + } + + counts.set(cell.path, (counts.get(cell.path) ?? 0) + 1); + } + } + + return [...counts.entries()].filter(([, count]) => count > 1).map(([entryPath]) => entryPath); +} + +function flattenRowPaths(rows) { + return rows.flatMap((row) => + LANGUAGE_COLUMNS.flatMap((language) => { + const cell = row.languages[language]; + return cell ? [cell.path] : []; + }), + ); +} + +function validateReadmeRows(rows) { + const problems = []; + + for (const row of rows) { + if (!row.name) { + problems.push("README has a template row with an empty name."); + } + + if (!row.description) { + problems.push(`README row \`${row.name}\` is missing a description.`); + } + + const linkedLanguages = LANGUAGE_COLUMNS.filter((language) => row.languages[language]); + if (linkedLanguages.length === 0) { + problems.push(`README row \`${row.name}\` must link at least one template.`); + } + + for (const language of LANGUAGE_COLUMNS) { + const cell = row.languages[language]; + if (!cell) { + continue; + } + + if (cell.label !== language) { + problems.push( + `README row \`${row.name}\` has a ${cell.label} link in the ${language} column.`, + ); + } + + const segments = cell.path.split("/"); + if (segments.length !== 2) { + problems.push( + `README entry \`${cell.path}\` must point to a first-level template directory.`, + ); + continue; + } + + const [root] = segments; + const expectedRoot = LANGUAGE_TO_ROOT[language]; + if (root !== expectedRoot) { + problems.push( + `README entry \`${cell.path}\` is in the ${language} column, but ${language} templates must live under \`${expectedRoot}/\`.`, + ); + } + + const directoryName = path.posix.basename(cell.path); + if (row.name !== directoryName) { + problems.push( + `README row name \`${row.name}\` must match directory name \`${directoryName}\`.`, + ); + } + } + } + + return problems; +} + +function getOrderMessage(expectedRows, readmeRows) { + const expectedNames = expectedRows.map((row) => row.name); + const actualNames = readmeRows.map((row) => row.name); + const mismatchIndex = expectedNames.findIndex( + (expectedName, index) => expectedName !== actualNames[index], + ); + + if (mismatchIndex === -1) { + return null; + } + + return [ + "README template rows are out of order.", + "Sort rows alphabetically by template name.", + `First mismatch at row ${mismatchIndex + 1}: expected \`${expectedNames[mismatchIndex]}\`, found \`${actualNames[mismatchIndex]}\`.`, + ].join("\n"); +} + +function main() { + const readmeContents = readFileSync(README_PATH, "utf8"); + const expectedRows = getExpectedRows(); + const readmeRows = parseReadmeRows(getReadmeSection(readmeContents)); + + const duplicateNames = findDuplicateNames(readmeRows); + const duplicatePaths = findDuplicatePaths(readmeRows); + const validationProblems = validateReadmeRows(readmeRows); + const expectedPaths = new Set(flattenRowPaths(expectedRows)); + const readmePaths = new Set(flattenRowPaths(readmeRows)); + const missingEntries = [...expectedPaths].filter((entry) => !readmePaths.has(entry)); + const unexpectedEntries = [...readmePaths].filter((entry) => !expectedPaths.has(entry)); + const orderProblem = getOrderMessage(expectedRows, readmeRows); + + const problems = []; + + if (duplicateNames.length > 0) { + problems.push( + `Duplicate README template rows found:\n${duplicateNames.map((name) => `- ${name}`).join("\n")}`, + ); + } + + if (duplicatePaths.length > 0) { + problems.push( + `Duplicate README entries found:\n${duplicatePaths.map((entryPath) => `- ${entryPath}`).join("\n")}`, + ); + } + + if (validationProblems.length > 0) { + problems.push(validationProblems.map((problem) => `- ${problem}`).join("\n")); + } + + if (missingEntries.length > 0) { + problems.push( + `Template directories missing from README:\n${missingEntries.map((entry) => `- ${entry}`).join("\n")}`, + ); + } + + if (unexpectedEntries.length > 0) { + problems.push( + `README entries that do not match any template directory:\n${unexpectedEntries.map((entry) => `- ${entry}`).join("\n")}`, + ); + } + + if (orderProblem) { + problems.push(orderProblem); + } + + if (problems.length > 0) { + fail( + ["README template index is out of sync with the template directory tree.", ...problems].join( + "\n\n", + ), + ); + } + + console.log( + `README template index matches ${expectedRows.length} template rows covering ${expectedPaths.size} first-level template directories.`, + ); +} + +main();