diff --git a/.github/workflows/auto-fix.yml b/.github/workflows/auto-fix.yml new file mode 100644 index 0000000..6d44d57 --- /dev/null +++ b/.github/workflows/auto-fix.yml @@ -0,0 +1,68 @@ +name: Auto-fix Code Issues + +on: + issue_comment: + types: [created] + +jobs: + auto-fix: + name: Auto-fix Code Issues + runs-on: ubuntu-latest + if: github.event.issue.pull_request && contains(github.event.comment.body, '/fix') + steps: + - name: Checkout PR code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.pull_request.head.ref }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Auto-fix linting issues + run: pnpm lint:fix + + - name: Auto-fix formatting issues + run: pnpm format + + - name: Commit and push changes + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add -A + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "🤖 Auto-fix linting and formatting issues" + git push + fi + + - name: Add reaction to comment + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + comment-id: ${{ github.event.comment.id }} + reactions: "+1" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1b65ab9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,164 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + lint-and-format: + name: Lint and Format + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run ESLint + run: pnpm lint + + - name: Check Prettier formatting + run: pnpm format:check + + - name: Run TypeScript type checking + run: pnpm typecheck + + build: + name: Build + runs-on: ubuntu-latest + needs: lint-and-format + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: pnpm build + + test: + name: Test + runs-on: ubuntu-latest + needs: lint-and-format + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm test + + security: + name: Security Audit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run security audit + run: pnpm audit --audit-level moderate + + - name: Run dependency vulnerabilities check + uses: actions/dependency-review-action@v4 + if: github.event_name == 'pull_request' diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml new file mode 100644 index 0000000..8018d64 --- /dev/null +++ b/.github/workflows/dependency-updates.yml @@ -0,0 +1,72 @@ +name: Dependency Updates + +on: + schedule: + # Run every Monday at 9:00 AM UTC + - cron: "0 9 * * 1" + workflow_dispatch: # Allow manual triggering + +jobs: + update-dependencies: + name: Update Dependencies + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Update dependencies + run: | + pnpm update --latest + pnpm install + + - name: Run tests to ensure everything still works + run: | + pnpm lint + pnpm typecheck + pnpm build + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore: update dependencies" + title: "chore: update dependencies" + body: | + 🤖 This PR updates dependencies to their latest versions. + + **Changes:** + - Updated all dependencies to their latest compatible versions + - Ran linting, type checking, and build to ensure compatibility + + Please review the changes and test the integrations before merging. + branch: chore/update-dependencies + delete-branch: true + base: master diff --git a/.github/workflows/integration-health.yml b/.github/workflows/integration-health.yml new file mode 100644 index 0000000..e681852 --- /dev/null +++ b/.github/workflows/integration-health.yml @@ -0,0 +1,132 @@ +name: Integration Health Check + +on: + schedule: + # Run every week on Wednesday at 10:00 AM UTC + - cron: "0 10 * * 3" + workflow_dispatch: # Allow manual triggering + +jobs: + check-integration-health: + name: Check Integration Health + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10.9.0 + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check for outdated Node.js dependencies + run: | + echo "## Node.js Dependencies Health Check" >> health-report.md + echo "" >> health-report.md + + # Find all package.json files (excluding node_modules) + find examples -name "package.json" -not -path "*/node_modules/*" | while read -r pkg; do + echo "### $(dirname "$pkg")" >> health-report.md + echo "" >> health-report.md + cd "$(dirname "$pkg")" || continue + + # Check for outdated dependencies + if pnpm outdated --format json > outdated.json 2>/dev/null && [ -s outdated.json ]; then + echo "⚠️ **Outdated dependencies found:**" >> ../../health-report.md + echo "\`\`\`json" >> ../../health-report.md + cat outdated.json >> ../../health-report.md + echo "\`\`\`" >> ../../health-report.md + else + echo "✅ All dependencies are up to date" >> ../../health-report.md + fi + echo "" >> ../../health-report.md + + rm -f outdated.json + cd - > /dev/null + done + + - name: Check for outdated Python dependencies + run: | + echo "## Python Dependencies Health Check" >> health-report.md + echo "" >> health-report.md + + # Find all requirements.txt files + find examples -name "requirements.txt" | while read -r req; do + echo "### $(dirname "$req")" >> health-report.md + echo "" >> health-report.md + + # Install pip-outdated for checking + pip install pip-outdated > /dev/null 2>&1 || true + + cd "$(dirname "$req")" || continue + + # Check for outdated Python packages + if pip-outdated requirements.txt > outdated.txt 2>/dev/null && [ -s outdated.txt ]; then + echo "⚠️ **Outdated Python packages found:**" >> ../../health-report.md + echo "\`\`\`" >> ../../health-report.md + cat outdated.txt >> ../../health-report.md + echo "\`\`\`" >> ../../health-report.md + else + echo "✅ All Python packages are up to date" >> ../../health-report.md + fi + echo "" >> ../../health-report.md + + rm -f outdated.txt + cd - > /dev/null + done + + - name: Check integration documentation freshness + run: | + echo "## Documentation Health Check" >> health-report.md + echo "" >> health-report.md + + # Check for README files that haven't been updated in 90 days + find examples -name "README.md" | while read -r readme; do + if [ -f "$readme" ]; then + # Get last modified date + last_modified=$(stat -c %Y "$readme" 2>/dev/null || stat -f %m "$readme" 2>/dev/null || echo "0") + current_time=$(date +%s) + days_old=$(( (current_time - last_modified) / 86400 )) + + if [ $days_old -gt 90 ]; then + echo "⚠️ **$(dirname "$readme")/README.md** - Last updated $days_old days ago" >> health-report.md + fi + fi + done + + - name: Create Issue with Health Report + uses: peter-evans/create-issue-from-file@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "Integration Health Report - $(date +%Y-%m-%d)" + content-filepath: health-report.md + labels: | + maintenance + dependencies + integrations diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..89d7972 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm pre-commit diff --git a/.prettierrc.json b/.prettierrc.json index 8d3a527..e493526 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,6 @@ { "semi": true, - "singleQuote": true, + "singleQuote": false, "tabWidth": 2, "trailingComma": "es5", "printWidth": 80, @@ -9,4 +9,4 @@ "bracketSpacing": true, "bracketSameLine": false, "quoteProps": "as-needed" -} \ No newline at end of file +} diff --git a/README.md b/README.md index 89c67ce..656f01a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Browserbase is headless browser infrastructure designed specifically for AI agen - **Browser sessions** that scale automatically - **Anti-detection capabilities** to bypass bot protection -- **Visual debugging** with session recordings and screenshots +- **Visual debugging** with session recordings and screenshots - **Global infrastructure** for low-latency access worldwide - **Stealth technology** to ensure reliable web interaction @@ -21,56 +21,68 @@ Our integrations are organized by platform and use case, each with comprehensive ### 🤖 AI Agent Frameworks #### [**CrewAI Integration**](./examples/integrations/crewai/README.md) + Enable your CrewAI agents to browse the web like humans with the `BrowserbaseLoadTool`. Perfect for creating intelligent agent crews that need to gather real-time web data, extract content from complex pages, and interact with modern web applications. **Key Features:** + - Extract text from JavaScript-heavy websites -- Capture screenshots and visual content +- Capture screenshots and visual content - Bypass anti-bot mechanisms - Seamless integration with CrewAI's tool ecosystem #### [**AgentKit Integration**](./examples/integrations/agentkit/) + Powerful integrations for AgentKit workflows with both Browserbase and Stagehand implementations: - **[Browserbase Implementation](./examples/integrations/agentkit/browserbase/README.md)** - Direct Browserbase integration for AgentKit - **[Stagehand Implementation](./examples/integrations/agentkit/stagehand/README.md)** - AI-powered web automation using Stagehand #### [**Agno Integration**](./examples/integrations/agno/README.md) + **Intelligent Web Scraping with AI Agents** - Natural language web scraping using Agno's AI agents powered by Browserbase's cloud browser infrastructure. Perfect for complex data extraction, market research, and automated content monitoring. **Key Features:** + - Natural language scraping instructions - AI agents that adapt to page changes - Visual analysis and screenshot capabilities - Structured data extraction (JSON, CSV) - Automatic error recovery and retries -#### [**LangChain Integration**](./examples/integrations/langchain/README.md) +#### [**LangChain Integration**](./examples/integrations/langchain/README.md) + Integrate Browserbase with LangChain's ecosystem for advanced AI applications. Build chains that can browse, extract, and interact with web content as part of larger AI workflows. #### [**Mastra Integration**](./examples/integrations/mastra/README.md) + Powerful web automation combining Browserbase's Stagehand with Mastra's AI agent framework. Enable your Mastra agents to navigate websites, extract data, and perform complex web interactions through natural language commands. **Key Features:** + - AI-powered web navigation and interaction -- Smart element observation and data extraction +- Smart element observation and data extraction - Session management with automatic timeouts - Natural language interface to web automation - Integration with OpenAI models for intelligent decision-making #### [**Browser-Use Integration**](./examples/integrations/browser-use/README.md) + Streamlined browser automation for AI applications with a focus on simplicity and reliability. #### [**Portia AI Integration**](./examples/integrations/portia/README.md) + Build intelligent web agents with **persistent authentication** using Portia AI's multi-agent framework. Portia enables both multi-agent task planning with human feedback and stateful multi-agent task execution with human control. **Key Features:** + - **Persistent Authentication** - Agents can authenticate once and reuse sessions - **Human-in-the-Loop** - Structured clarification system for authentication requests - **Multi-User Support** - Isolated browser sessions per end user - **Production-Ready** - Open-source framework designed for reliable agent deployment **Perfect for:** + - LinkedIn automation with user authentication - E-commerce agents that need to log into shopping sites - Data extraction from authenticated dashboards @@ -79,16 +91,20 @@ Build intelligent web agents with **persistent authentication** using Portia AI' ### 🏗️ Development & Deployment Platforms #### [**Vercel AI Integration**](./examples/integrations/vercel/README.md) + Enhance your Vercel applications with web-browsing capabilities. Build Generative User Interfaces that can access real-time web data and create dynamic, AI-powered experiences. **Examples Include:** + - **BrowseGPT** - A chat interface with real-time web search capabilities - **Vercel + Puppeteer** - Server-side browser automation on Fluid Compute #### [**Trigger.dev Integration**](./examples/integrations/trigger/README.md) + **Background Jobs & Web Automation** - Build robust background task workflows with Trigger.dev's job orchestration platform. Combine Browserbase's web automation capabilities with scheduled tasks, retry logic, and complex multi-step workflows. **Key Features:** + - **Scheduled Web Scraping** - Automated data collection with cron-based scheduling - **PDF Processing Pipelines** - Convert documents and upload to cloud storage - **AI-Powered Content Workflows** - Scrape, summarize, and distribute content via email @@ -96,17 +112,20 @@ Enhance your Vercel applications with web-browsing capabilities. Build Generativ - **Production-Grade Reliability** - Built-in retries, error handling, and observability **Perfect for:** + - Automated market research and competitive analysis -- Document processing and content generation workflows +- Document processing and content generation workflows - Scheduled reporting and email automation - Complex web automation pipelines that require orchestration ### 💳 E-commerce & Payments #### [**Stripe Integration**](./examples/integrations/stripe/README.md) + **Agentic Credit Card Automation** - Create virtual cards with Stripe Issuing and automate online purchases with Browserbase. Perfect for programmatic commerce, testing payment flows, and building AI shopping agents. **Capabilities:** + - Create virtual cards with spending controls - Automate secure online purchases - Available in Node.js, Python, and Stagehand implementations @@ -115,12 +134,15 @@ Enhance your Vercel applications with web-browsing capabilities. Build Generativ ### 📊 Evaluation & Testing #### [**Braintrust Integration**](./examples/integrations/braintrust/README.md) + Integrate Browserbase with Braintrust for evaluation and testing of AI agent performance in web environments. Monitor, measure, and improve your browser automation workflows. #### [**MongoDB Integration**](./examples/integrations/mongodb/README.md) + **Intelligent Web Scraping & Data Storage** - Extract structured data from e-commerce websites using Stagehand and store it in MongoDB for analysis. Perfect for building data pipelines, market research, and competitive analysis workflows. **Capabilities:** + - AI-powered web scraping with Stagehand - Structured data extraction with schema validation - MongoDB storage for persistence and querying @@ -136,7 +158,7 @@ integrations/ │ └── community/ # WIP │ └── integrations/ │ ├── crewai/ # CrewAI framework integration -│ ├── vercel/ # Vercel AI SDK integration +│ ├── vercel/ # Vercel AI SDK integration │ ├── trigger/ # Trigger.dev background jobs & automation │ ├── stripe/ # Stripe Issuing + automation │ ├── langchain/ # LangChain framework integration @@ -158,8 +180,9 @@ integrations/ 4. **Review the code samples** to understand implementation patterns Each integration includes: + - ✅ Complete setup instructions -- ✅ Environment configuration guides +- ✅ Environment configuration guides - ✅ Working code examples - ✅ Best practices and tips - ✅ Troubleshooting guides @@ -167,6 +190,7 @@ Each integration includes: ## 🔧 Prerequisites Most integrations require: + - A [Browserbase account](https://browserbase.com) and API key - Node.js 18+ or Python 3.8+ (depending on implementation) - Framework-specific dependencies (detailed in each integration) @@ -180,11 +204,14 @@ For comprehensive documentation, tutorials, and API references, visit: ## 🤝 Community & Support ### Get Help + - **📧 Email Support**: [support@browserbase.com](mailto:support@browserbase.com) - **📚 Documentation**: [docs.browserbase.com](https://docs.browserbase.com) ### Contributing + We welcome contributions! Each integration has its own contribution guidelines. Feel free to: + - Report bugs and request features - Submit pull requests with improvements - Share your own integration examples @@ -196,4 +223,4 @@ This project is licensed under the MIT License. See individual integration direc --- -**Built with ❤️ by the Browserbase team** \ No newline at end of file +**Built with ❤️ by the Browserbase team** diff --git a/eslint.config.js b/eslint.config.js index 432dab5..e886268 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,25 +1,31 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import js from "@eslint/js"; +import globals from "globals"; +import tseslint from "typescript-eslint"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; /** @type {import('eslint').Linter.Config[]} */ export default [ { ignores: [ - 'coverage/**', - '**/dist/**', - '**/build/**', - '**/out/**', - '**/public/**', - 'pnpm-lock.yaml', - 'pnpm-workspace.yaml', - '**/node_modules/**', - '**/.pnpm-store/**', - '**/*.min.js', - '**/*.min.css', - '**/.next/**', - '**/.nuxt/**', + "coverage/**", + "**/dist/**", + "**/build/**", + "**/out/**", + "**/public/**", + "pnpm-lock.yaml", + "pnpm-workspace.yaml", + "**/node_modules/**", + "**/.pnpm-store/**", + "**/*.min.js", + "**/*.min.css", + "**/.next/**", + "**/.nuxt/**", + "**/.venv/**", + "**/venv/**", + "**/__pycache__/**", + "**/*.pyc", + "**/lib/**", + "**/tsconfig.tsbuildinfo", ], }, js.configs.recommended, @@ -34,17 +40,28 @@ export default [ }, }, { - files: ['**/*.{js,mjs,cjs,ts,tsx}'], + files: ["**/*.{js,mjs,cjs,ts,tsx}"], rules: { - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_' }, + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "warn", + "@typescript-eslint/no-unused-expressions": "warn", + "@typescript-eslint/no-require-imports": "warn", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "prefer-const": "error", + "no-var": "error", + "no-undef": "warn", + "prettier/prettier": [ + "error", + { + singleQuote: false, + trailingComma: "es5", + }, ], - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-non-null-assertion': 'warn', - 'prefer-const': 'error', - 'no-var': 'error', }, }, eslintPluginPrettierRecommended, -]; \ No newline at end of file +]; diff --git a/examples/community/README.md b/examples/community/README.md index 20ce205..8f22578 100644 --- a/examples/community/README.md +++ b/examples/community/README.md @@ -1,3 +1,3 @@ # WIP -For community integrations \ No newline at end of file +For community integrations diff --git a/examples/integrations/agentkit/README.md b/examples/integrations/agentkit/README.md index 67ce4b9..19ef372 100644 --- a/examples/integrations/agentkit/README.md +++ b/examples/integrations/agentkit/README.md @@ -11,7 +11,7 @@ By the end of this guide, you'll have an AI agent built with AgentKit that can b ### You'll learn how to: - Create AgentKit tools that leverage Browserbase's managed headless browsers -- Build autonomous web browsing agents that can search, extract data, and interact with websites +- Build autonomous web browsing agents that can search, extract data, and interact with websites - Use Stagehand, Browserbase's AI-powered browser automation library, to create resilient web agents ### This integration is useful for: @@ -33,7 +33,7 @@ There are two approaches to using Browserbase with AgentKit: Before you start, make sure you have: - AgentKit installed -- Browserbase Project ID & API key +- Browserbase Project ID & API key - (Optional) LLM API key of your choice to use with Stagehand Next, let's dive into building web browsing agents with AgentKit and Browserbase. diff --git a/examples/integrations/agentkit/browserbase/README.md b/examples/integrations/agentkit/browserbase/README.md index 5e56ffa..19b653c 100644 --- a/examples/integrations/agentkit/browserbase/README.md +++ b/examples/integrations/agentkit/browserbase/README.md @@ -82,4 +82,4 @@ From there, trigger the `reddit-search-network` function with your query: ## License -[MIT License](LICENSE) \ No newline at end of file +[MIT License](LICENSE) diff --git a/examples/integrations/agentkit/browserbase/src/index.ts b/examples/integrations/agentkit/browserbase/src/index.ts index 51f1279..d36a8b4 100644 --- a/examples/integrations/agentkit/browserbase/src/index.ts +++ b/examples/integrations/agentkit/browserbase/src/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ import "dotenv/config"; import { anthropic, @@ -45,7 +44,7 @@ const searchReddit = createTool({ // Extract search results const results = await page.evaluate(() => { const posts = document.querySelectorAll("div.results div:has(h1)"); - return Array.from(posts).map((post) => ({ + return Array.from(posts).map(post => ({ title: post.querySelector("h1")?.textContent?.trim(), content: post.querySelector("div")?.textContent?.trim(), })); diff --git a/examples/integrations/agentkit/browserbase/tsconfig.json b/examples/integrations/agentkit/browserbase/tsconfig.json index 0313b2e..b745615 100644 --- a/examples/integrations/agentkit/browserbase/tsconfig.json +++ b/examples/integrations/agentkit/browserbase/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "target": "es2023", - "module": "Node16", - "lib": ["es2023", "DOM"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "Node16" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.test.ts"] - } \ No newline at end of file + "compilerOptions": { + "target": "es2023", + "module": "Node16", + "lib": ["es2023", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "Node16" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/examples/integrations/agentkit/stagehand/README.md b/examples/integrations/agentkit/stagehand/README.md index 9ec3f08..eadf725 100644 --- a/examples/integrations/agentkit/stagehand/README.md +++ b/examples/integrations/agentkit/stagehand/README.md @@ -97,4 +97,4 @@ The workflow includes proper error handling and cleanup: ## License -MIT \ No newline at end of file +MIT diff --git a/examples/integrations/agentkit/stagehand/src/index.ts b/examples/integrations/agentkit/stagehand/src/index.ts index 2a9021d..cc23339 100644 --- a/examples/integrations/agentkit/stagehand/src/index.ts +++ b/examples/integrations/agentkit/stagehand/src/index.ts @@ -143,4 +143,4 @@ const server = createServer({ server.listen(3010, () => console.log("Simple search Agent demo server is running on port 3010") -); \ No newline at end of file +); diff --git a/examples/integrations/agentkit/stagehand/src/stagehand-tools.ts b/examples/integrations/agentkit/stagehand/src/stagehand-tools.ts index 1dfe706..2e0e9a6 100644 --- a/examples/integrations/agentkit/stagehand/src/stagehand-tools.ts +++ b/examples/integrations/agentkit/stagehand/src/stagehand-tools.ts @@ -94,4 +94,4 @@ export const observe = createTool({ } }); }, -}); \ No newline at end of file +}); diff --git a/examples/integrations/agentkit/stagehand/src/utils.ts b/examples/integrations/agentkit/stagehand/src/utils.ts index 84686ed..4030f0f 100644 --- a/examples/integrations/agentkit/stagehand/src/utils.ts +++ b/examples/integrations/agentkit/stagehand/src/utils.ts @@ -97,4 +97,4 @@ export function isLastMessageOfType( type: MessageType ) { return result.output[result.output.length - 1]?.type === type; -} \ No newline at end of file +} diff --git a/examples/integrations/agentkit/stagehand/tsconfig.json b/examples/integrations/agentkit/stagehand/tsconfig.json index 0313b2e..b745615 100644 --- a/examples/integrations/agentkit/stagehand/tsconfig.json +++ b/examples/integrations/agentkit/stagehand/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "target": "es2023", - "module": "Node16", - "lib": ["es2023", "DOM"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "Node16" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.test.ts"] - } \ No newline at end of file + "compilerOptions": { + "target": "es2023", + "module": "Node16", + "lib": ["es2023", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "Node16" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/examples/integrations/agno/README.md b/examples/integrations/agno/README.md index a46cf62..f1a8f81 100644 --- a/examples/integrations/agno/README.md +++ b/examples/integrations/agno/README.md @@ -7,11 +7,13 @@ Agno provides AI agents that can understand natural language instructions for we ## 🚀 Why This Integration? ### Traditional Scraping Challenges + - **JavaScript-Heavy Sites**: Content loads dynamically after page load -- **Anti-Bot Protection**: Advanced detection systems block traditional scrapers +- **Anti-Bot Protection**: Advanced detection systems block traditional scrapers - **Infrastructure Complexity**: Managing browsers and scaling is difficult ### Agno + Browserbase Solution + - **🤖 AI-Powered**: Natural language scraping instructions - **🚀 Real Browser**: Full Chrome with JavaScript execution - **🛡️ Stealth Capabilities**: Bypasses anti-bot systems @@ -23,26 +25,28 @@ Agno provides AI agents that can understand natural language instructions for we **Visual Analysis**: Screenshots, layout detection, visual regression testing **Multi-Step Navigation**: Handle pagination, forms, and complex user journeys **Structured Data**: Extract data in JSON, CSV, or custom formats -**Error Recovery**: Automatic retries and intelligent error handling +**Error Recovery**: Automatic retries and intelligent error handling ## 🔧 Setup **Install packages:** + ```bash pip install browserbase playwright agno ``` **Set environment variables:** + ```bash export BROWSERBASE_API_KEY=your_api_key_here export BROWSERBASE_PROJECT_ID=your_project_id_here ``` -*Get credentials from the [Browserbase dashboard](https://browserbase.com)* +_Get credentials from the [Browserbase dashboard](https://browserbase.com)_ ## 🤝 Support & Resources - **📧 Support**: [support@browserbase.com](mailto:support@browserbase.com) - **📚 Documentation**: [docs.browserbase.com](https://docs.browserbase.com) - **🔧 Agno Docs**: [agno documentation](https://agno.dev) -- **💬 Community**: [GitHub Issues](https://github.com/browserbase/integrations/issues) \ No newline at end of file +- **💬 Community**: [GitHub Issues](https://github.com/browserbase/integrations/issues) diff --git a/examples/integrations/braintrust/README.md b/examples/integrations/braintrust/README.md index 770f0f0..a0d77d5 100644 --- a/examples/integrations/braintrust/README.md +++ b/examples/integrations/braintrust/README.md @@ -9,7 +9,7 @@ Braintrust is a platform for building AI applications, making it more efficient ### Key Features - Prototyping with different prompts and LLMs in a sandboxed environment -- Real-time monitoring and performance insights +- Real-time monitoring and performance insights - Data management through intuitive UI ## Tool Calling Support @@ -27,4 +27,4 @@ Custom functions enhance LLM capabilities by enabling: - Web-browsing capabilities (using tools like Browserbase) - Complex computations -- External API access \ No newline at end of file +- External API access diff --git a/examples/integrations/braintrust/src/api.ts b/examples/integrations/braintrust/src/api.ts index 714684d..feeca98 100644 --- a/examples/integrations/braintrust/src/api.ts +++ b/examples/integrations/braintrust/src/api.ts @@ -11,7 +11,7 @@ async function createSession() { "Content-Type": "application/json", }, body: JSON.stringify({ - projectId: process.env.BROWSERBASE_PROJECT_ID!, + projectId: process.env.BROWSERBASE_PROJECT_ID, proxies: true, }), }); @@ -23,7 +23,7 @@ async function createSession() { async function loadPage({ url }: { url: string }) { const { id } = await createSession(); const browser = await chromium.connectOverCDP( - `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}&sessionId=${id}`, + `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}&sessionId=${id}` ); const defaultContext = browser.contexts()[0]; @@ -56,4 +56,4 @@ project.tools.create({ slug: "load-page", description: "Load a page from the internet", ifExists: "replace", -}); \ No newline at end of file +}); diff --git a/examples/integrations/braintrust/src/sdk.ts b/examples/integrations/braintrust/src/sdk.ts index 0795f27..d13c0a0 100644 --- a/examples/integrations/braintrust/src/sdk.ts +++ b/examples/integrations/braintrust/src/sdk.ts @@ -5,8 +5,8 @@ import Browserbase from "@browserbasehq/sdk"; import { chromium } from "playwright-core"; import { z } from "zod"; -const BROWSERBASE_API_KEY = process.env.BROWSERBASE_API_KEY!; -const BROWSERBASE_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID!; +const BROWSERBASE_API_KEY = process.env.BROWSERBASE_API_KEY; +const BROWSERBASE_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID; async function load({ url }: { url: string }): Promise<{ page: string; @@ -15,7 +15,7 @@ async function load({ url }: { url: string }): Promise<{ apiKey: BROWSERBASE_API_KEY, }); const session = await bb.sessions.create({ - projectId: BROWSERBASE_PROJECT_ID, + projectId: BROWSERBASE_PROJECT_ID || "", }); const browser = await chromium.connectOverCDP(session.connectUrl); const context = browser.contexts()[0]; @@ -41,4 +41,4 @@ project.tools.create({ slug: "load-page", description: "Load a page from the internet", ifExists: "replace", -}); \ No newline at end of file +}); diff --git a/examples/integrations/braintrust/tsconfig.json b/examples/integrations/braintrust/tsconfig.json index 0313b2e..b745615 100644 --- a/examples/integrations/braintrust/tsconfig.json +++ b/examples/integrations/braintrust/tsconfig.json @@ -1,16 +1,16 @@ { - "compilerOptions": { - "target": "es2023", - "module": "Node16", - "lib": ["es2023", "DOM"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "Node16" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "**/*.test.ts"] - } \ No newline at end of file + "compilerOptions": { + "target": "es2023", + "module": "Node16", + "lib": ["es2023", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "Node16" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "**/*.test.ts"] +} diff --git a/examples/integrations/browser-use/README.md b/examples/integrations/browser-use/README.md index 0efb35d..1b953b2 100644 --- a/examples/integrations/browser-use/README.md +++ b/examples/integrations/browser-use/README.md @@ -2,4 +2,4 @@ Utilize Browserbase with Browser Use for advanced browser automation. -Browser Use is a browser automation sdk that uses screenshots to capture the state of the browser and actions to simulate user interactions. \ No newline at end of file +Browser Use is a browser automation sdk that uses screenshots to capture the state of the browser and actions to simulate user interactions. diff --git a/examples/integrations/crewai/README.md b/examples/integrations/crewai/README.md index 55e04f0..6432e60 100644 --- a/examples/integrations/crewai/README.md +++ b/examples/integrations/crewai/README.md @@ -28,7 +28,7 @@ This tool integrates the Stagehand Python SDK with CrewAI, allowing agents to in The StagehandTool wraps the Stagehand Python SDK to provide CrewAI agents with the ability to control a real web browser and interact with websites using three core primitives: - **Act**: Perform actions like clicking, typing, or navigating -- **Extract**: Extract structured data from web pages +- **Extract**: Extract structured data from web pages - **Observe**: Identify and analyze elements on the page ### Requirements diff --git a/examples/integrations/crewai/crewai-tutorial/README.md b/examples/integrations/crewai/crewai-tutorial/README.md index 0c3f622..d6df357 100644 --- a/examples/integrations/crewai/crewai-tutorial/README.md +++ b/examples/integrations/crewai/crewai-tutorial/README.md @@ -1,4 +1,5 @@ # Tutorial: build a Flight Booking Crew + ### Build a Crew that finds the best roundtrip flights on the given dates. This is based off the guide in the [Browserbase docs](https://docs.browserbase.com/integrations/crew-ai/build-a-flight-booker) @@ -20,4 +21,3 @@ OPENAI_MODEL_NAME=gpt-4-turbo ## Running the Crew To run the Crew, run `poetry run python main.py "flights from SF to New York on November 5th"` - diff --git a/examples/integrations/crewai/quickstart/README.md b/examples/integrations/crewai/quickstart/README.md index a0941f9..c75702d 100644 --- a/examples/integrations/crewai/quickstart/README.md +++ b/examples/integrations/crewai/quickstart/README.md @@ -1,3 +1,3 @@ # Quickstart -Get started quickly using the BrowserbaseLoadTool from CrewAI \ No newline at end of file +Get started quickly using the BrowserbaseLoadTool from CrewAI diff --git a/examples/integrations/crewai/stagehand/README.md b/examples/integrations/crewai/stagehand/README.md index d162838..2bcc2d5 100644 --- a/examples/integrations/crewai/stagehand/README.md +++ b/examples/integrations/crewai/stagehand/README.md @@ -9,7 +9,7 @@ This tool integrates the Stagehand Python SDK with CrewAI, allowing agents to in The StagehandTool wraps the Stagehand Python SDK to provide CrewAI agents with the ability to control a real web browser and interact with websites using three core primitives: - **Act**: Perform actions like clicking, typing, or navigating -- **Extract**: Extract structured data from web pages +- **Extract**: Extract structured data from web pages - **Observe**: Identify and analyze elements on the page ## Requirements @@ -18,4 +18,4 @@ Before using this tool, you will need: - A Browserbase account with API key and project ID - An API key for an LLM (OpenAI or Anthropic Claude) -- The Stagehand Python SDK installed \ No newline at end of file +- The Stagehand Python SDK installed diff --git a/examples/integrations/langchain/README.md b/examples/integrations/langchain/README.md index eb1c084..4e34f2b 100644 --- a/examples/integrations/langchain/README.md +++ b/examples/integrations/langchain/README.md @@ -7,7 +7,8 @@ This directory contains examples of integrating Langchain with our web automatio 2. **Stagehand Integration**: Full web automation capabilities using our open-source AI-powered browser automation SDK. Choose the example that best fits your needs: + - Use Browserbase for simple web scraping and data collection - Use Stagehand for complex automation workflows with AI-driven interactions -See the respective directories for detailed implementation guides. \ No newline at end of file +See the respective directories for detailed implementation guides. diff --git a/examples/integrations/langchain/browserbase/README.md b/examples/integrations/langchain/browserbase/README.md index 2691ef4..c465f06 100644 --- a/examples/integrations/langchain/browserbase/README.md +++ b/examples/integrations/langchain/browserbase/README.md @@ -9,7 +9,7 @@ Langchain is a Python framework to build applications on top of large-language m Building on top of LLMs comes with many challenges: - Gathering and preparing the data (context) and providing memory to models -- Orchestrating tasks to match LLM API requirements (ex, rate limiting, chunking) +- Orchestrating tasks to match LLM API requirements (ex, rate limiting, chunking) - Parse the different LLM result format Langchain comes with a set of high-level concepts and tools to cope with those challenges: @@ -22,4 +22,4 @@ Langchain comes with a set of high-level concepts and tools to cope with those c Browserbase provides a Document Loader to enable your Langchain application to browse the web to: - Extract text or raw HTML, including from web pages using JavaScript or dynamically rendered text -- Load images via screenshots \ No newline at end of file +- Load images via screenshots diff --git a/examples/integrations/langchain/stagehand/README.md b/examples/integrations/langchain/stagehand/README.md index 41d8014..cc505a4 100644 --- a/examples/integrations/langchain/stagehand/README.md +++ b/examples/integrations/langchain/stagehand/README.md @@ -18,6 +18,7 @@ For more details on this integration and how to work with Langchain, see the off ## Remote Browsers (Browserbase) Instead of `env: "LOCAL"`, specify `env: "BROWSERBASE"` and pass in your Browserbase credentials through environment variables: + - `BROWSERBASE_API_KEY` - `BROWSERBASE_PROJECT_ID` @@ -31,4 +32,4 @@ With the StagehandToolkit, you can quickly integrate natural-language-driven bro - Automating login flows - Navigating or clicking through forms based on instructions from a larger chain of agents -Consult Stagehand's and Langchain's official references for troubleshooting and advanced integrations or reach out to us on [Slack](https://stagehand.dev/slack). \ No newline at end of file +Consult Stagehand's and Langchain's official references for troubleshooting and advanced integrations or reach out to us on [Slack](https://stagehand.dev/slack). diff --git a/examples/integrations/langchain/stagehand/src/index.js b/examples/integrations/langchain/stagehand/src/index.js index 4ba9c95..aaf6c8f 100644 --- a/examples/integrations/langchain/stagehand/src/index.js +++ b/examples/integrations/langchain/stagehand/src/index.js @@ -1,8 +1,8 @@ -import { Stagehand } from '@browserbasehq/stagehand'; -import { StagehandToolkit } from '@stagehand/langchain'; +import { Stagehand } from "@browserbasehq/stagehand"; +import { StagehandToolkit } from "@stagehand/langchain"; const stagehand = new Stagehand({ - env: 'LOCAL', + env: "LOCAL", verbose: 2, enableCaching: false, }); @@ -11,28 +11,28 @@ const stagehandToolkit = await StagehandToolkit.fromStagehand(stagehand); // Find the relevant tool const navigateTool = stagehandToolkit.tools.find( - t => t.name === 'stagehand_navigate' + t => t.name === "stagehand_navigate" ); // Invoke it -await navigateTool.invoke('https://www.google.com'); +await navigateTool.invoke("https://www.google.com"); // Suppose you want to act on the page -const actionTool = stagehandToolkit.tools.find(t => t.name === 'stagehand_act'); +const actionTool = stagehandToolkit.tools.find(t => t.name === "stagehand_act"); await actionTool.invoke('Search for "OpenAI"'); // Observe the current page const observeTool = stagehandToolkit.tools.find( - t => t.name === 'stagehand_observe' + t => t.name === "stagehand_observe" ); const result = await observeTool.invoke( - 'What actions can be performed on the current page?' + "What actions can be performed on the current page?" ); console.log(JSON.parse(result)); // Verification const currentUrl = stagehand.page.url(); -console.log('Current URL:', currentUrl); +console.log("Current URL:", currentUrl); diff --git a/examples/integrations/mastra/README.md b/examples/integrations/mastra/README.md index f67957f..602c6f0 100644 --- a/examples/integrations/mastra/README.md +++ b/examples/integrations/mastra/README.md @@ -21,18 +21,20 @@ This project enables AI agents to interact with web pages through the Mastra fra - Node.js (v18+) - npm or yarn -- Browserbase account +- Browserbase account - OpenAI API access ### Setup 1. Clone the repository: + ``` git clone https://github.com/browserbase/Stagehand-Mastra-App.git cd Stagehand-Mastra-App ``` 2. Install dependencies: + ``` npm install ``` @@ -59,11 +61,13 @@ This will start the Mastra development server, giving you access to the integrat ### Core Components 1. **Stagehand Session Manager** + - Handles browser session initialization and management - Implements automatic session timeouts - Provides error recovery and reconnection logic 2. **Mastra Tools** + - `stagehandActTool`: Performs actions on web pages - `stagehandObserveTool`: Identifies elements on web pages - `stagehandExtractTool`: Extracts data from web pages @@ -86,6 +90,7 @@ The project can be configured through the `.env` file and by modifying the agent ## Credits This project is built with: + - [Mastra](https://mastra.ai) - AI Agent framework - [Stagehand by Browserbase](https:/stagehand.dev) - Browser automation -- [OpenAI](https://openai.com/) - AI models \ No newline at end of file +- [OpenAI](https://openai.com/) - AI models diff --git a/examples/integrations/mastra/src/mastra/agents/index.ts b/examples/integrations/mastra/src/mastra/agents/index.ts index 8aea466..b14e266 100644 --- a/examples/integrations/mastra/src/mastra/agents/index.ts +++ b/examples/integrations/mastra/src/mastra/agents/index.ts @@ -1,12 +1,16 @@ -import { openai } from '@ai-sdk/openai'; -import { Agent } from '@mastra/core/agent'; -import { stagehandActTool, stagehandObserveTool, stagehandExtractTool } from '../tools'; -import { Memory } from '@mastra/memory'; +import { openai } from "@ai-sdk/openai"; +import { Agent } from "@mastra/core/agent"; +import { + stagehandActTool, + stagehandObserveTool, + stagehandExtractTool, +} from "../tools"; +import { Memory } from "@mastra/memory"; const memory = new Memory(); export const webAgent = new Agent({ - name: 'Web Assistant', + name: "Web Assistant", instructions: ` You are a helpful web assistant that can navigate websites and extract information. @@ -25,7 +29,7 @@ export const webAgent = new Agent({ Use the stagehandObserveTool to find elements on webpages. Use the stagehandExtractTool to extract data from webpages. `, - model: openai('gpt-4o'), + model: openai("gpt-4o"), tools: { stagehandActTool, stagehandObserveTool, stagehandExtractTool }, - memory: memory + memory: memory, }); diff --git a/examples/integrations/mastra/src/mastra/index.ts b/examples/integrations/mastra/src/mastra/index.ts index ec2afa0..72df662 100644 --- a/examples/integrations/mastra/src/mastra/index.ts +++ b/examples/integrations/mastra/src/mastra/index.ts @@ -1,12 +1,12 @@ -import { Mastra } from '@mastra/core/mastra'; -import { createLogger } from '@mastra/core/logger'; +import { Mastra } from "@mastra/core/mastra"; +import { createLogger } from "@mastra/core/logger"; -import { webAgent } from './agents'; +import { webAgent } from "./agents"; export const mastra = new Mastra({ agents: { webAgent }, logger: createLogger({ - name: 'Mastra', - level: 'info', + name: "Mastra", + level: "info", }), }); diff --git a/examples/integrations/mastra/src/mastra/tools/index.ts b/examples/integrations/mastra/src/mastra/tools/index.ts index ea0ba2e..79c9ed9 100644 --- a/examples/integrations/mastra/src/mastra/tools/index.ts +++ b/examples/integrations/mastra/src/mastra/tools/index.ts @@ -1,5 +1,5 @@ -import { createTool } from '@mastra/core/tools'; -import { z } from 'zod'; +import { createTool } from "@mastra/core/tools"; +import { z } from "zod"; import { Stagehand } from "@browserbasehq/stagehand"; class StagehandSessionManager { @@ -33,35 +33,37 @@ class StagehandSessionManager { try { // Initialize if not already initialized if (!this.stagehand || !this.initialized) { - console.log('Creating new Stagehand instance'); + console.log("Creating new Stagehand instance"); this.stagehand = new Stagehand({ apiKey: process.env.BROWSERBASE_API_KEY!, projectId: process.env.BROWSERBASE_PROJECT_ID!, env: "BROWSERBASE", }); - + try { - console.log('Initializing Stagehand...'); + console.log("Initializing Stagehand..."); await this.stagehand.init(); - console.log('Stagehand initialized successfully'); + console.log("Stagehand initialized successfully"); this.initialized = true; return this.stagehand; } catch (initError) { - console.error('Failed to initialize Stagehand:', initError); + console.error("Failed to initialize Stagehand:", initError); throw initError; } } try { const title = await this.stagehand.page.evaluate(() => document.title); - console.log('Session check successful, page title:', title); + console.log("Session check successful, page title:", title); return this.stagehand; } catch (error) { // If we get an error indicating the session is invalid, reinitialize - console.error('Session check failed:', error); + console.error("Session check failed:", error); if ( error instanceof Error && - (error.message.includes("Target page, context or browser has been closed") || + (error.message.includes( + "Target page, context or browser has been closed" + ) || error.message.includes("Session expired") || error.message.includes("context destroyed")) ) { @@ -81,7 +83,9 @@ class StagehandSessionManager { this.initialized = false; this.stagehand = null; const errorMsg = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to initialize/reinitialize Stagehand: ${errorMsg}`); + throw new Error( + `Failed to initialize/reinitialize Stagehand: ${errorMsg}` + ); } } @@ -90,10 +94,10 @@ class StagehandSessionManager { */ private async checkAndCleanupSession(): Promise { if (!this.stagehand || !this.initialized) return; - + const now = Date.now(); if (now - this.lastUsed > this.sessionTimeout) { - console.log('Cleaning up idle Stagehand session'); + console.log("Cleaning up idle Stagehand session"); try { await this.stagehand.close(); } catch (error) { @@ -124,11 +128,18 @@ class StagehandSessionManager { const sessionManager = StagehandSessionManager.getInstance(); export const stagehandActTool = createTool({ - id: 'web-act', - description: 'Take an action on a webpage using Stagehand', + id: "web-act", + description: "Take an action on a webpage using Stagehand", inputSchema: z.object({ - url: z.string().optional().describe('URL to navigate to (optional if already on a page)'), - action: z.string().describe('Action to perform (e.g., "click sign in button", "type hello in search field")'), + url: z + .string() + .optional() + .describe("URL to navigate to (optional if already on a page)"), + action: z + .string() + .describe( + 'Action to perform (e.g., "click sign in button", "type hello in search field")' + ), }), outputSchema: z.object({ success: z.boolean(), @@ -140,37 +151,55 @@ export const stagehandActTool = createTool({ }); export const stagehandObserveTool = createTool({ - id: 'web-observe', - description: 'Observe elements on a webpage using Stagehand to plan actions', + id: "web-observe", + description: "Observe elements on a webpage using Stagehand to plan actions", inputSchema: z.object({ - url: z.string().optional().describe('URL to navigate to (optional if already on a page)'), - instruction: z.string().describe('What to observe (e.g., "find the sign in button")'), + url: z + .string() + .optional() + .describe("URL to navigate to (optional if already on a page)"), + instruction: z + .string() + .describe('What to observe (e.g., "find the sign in button")'), }), - outputSchema: z.array(z.any()).describe('Array of observable actions'), + outputSchema: z.array(z.any()).describe("Array of observable actions"), execute: async ({ context }) => { return await performWebObservation(context.url, context.instruction); }, }); export const stagehandExtractTool = createTool({ - id: 'web-extract', - description: 'Extract data from a webpage using Stagehand', + id: "web-extract", + description: "Extract data from a webpage using Stagehand", inputSchema: z.object({ - url: z.string().optional().describe('URL to navigate to (optional if already on a page)'), - instruction: z.string().describe('What to extract (e.g., "extract all product prices")'), - schema: z.record(z.any()).optional().describe('Zod schema definition for data extraction'), - useTextExtract: z.boolean().optional().describe('Set true for larger-scale extractions, false for small extractions'), + url: z + .string() + .optional() + .describe("URL to navigate to (optional if already on a page)"), + instruction: z + .string() + .describe('What to extract (e.g., "extract all product prices")'), + schema: z + .record(z.any()) + .optional() + .describe("Zod schema definition for data extraction"), + useTextExtract: z + .boolean() + .optional() + .describe( + "Set true for larger-scale extractions, false for small extractions" + ), }), - outputSchema: z.any().describe('Extracted data according to schema'), + outputSchema: z.any().describe("Extracted data according to schema"), execute: async ({ context }) => { // Create a default schema if none is provided const defaultSchema = { - content: z.string() + content: z.string(), }; - + return await performWebExtraction( - context.url, - context.instruction, + context.url, + context.instruction, context.schema || defaultSchema, context.useTextExtract ); @@ -180,21 +209,21 @@ export const stagehandExtractTool = createTool({ const performWebAction = async (url?: string, action?: string) => { const stagehand = await sessionManager.ensureStagehand(); const page = stagehand.page; - + try { // Navigate to the URL if provided if (url) { await page.goto(url); } - + // Perform the action if (action) { await page.act(action); } - - return { - success: true, - message: `Successfully performed: ${action}` + + return { + success: true, + message: `Successfully performed: ${action}`, }; } catch (error: any) { throw new Error(`Stagehand action failed: ${error.message}`); @@ -202,21 +231,23 @@ const performWebAction = async (url?: string, action?: string) => { }; const performWebObservation = async (url?: string, instruction?: string) => { - console.log(`Starting observation${url ? ` for ${url}` : ''} with instruction: ${instruction}`); - + console.log( + `Starting observation${url ? ` for ${url}` : ""} with instruction: ${instruction}` + ); + try { const stagehand = await sessionManager.ensureStagehand(); if (!stagehand) { - console.error('Failed to get Stagehand instance'); - throw new Error('Failed to get Stagehand instance'); + console.error("Failed to get Stagehand instance"); + throw new Error("Failed to get Stagehand instance"); } - + const page = stagehand.page; if (!page) { - console.error('Page not available'); - throw new Error('Page not available'); + console.error("Page not available"); + throw new Error("Page not available"); } - + try { // Navigate to the URL if provided if (url) { @@ -224,23 +255,25 @@ const performWebObservation = async (url?: string, instruction?: string) => { await page.goto(url); console.log(`Successfully navigated to ${url}`); } - + // Observe the page if (instruction) { console.log(`Observing with instruction: ${instruction}`); try { const actions = await page.observe(instruction); - console.log(`Observation successful, found ${actions.length} actions`); + console.log( + `Observation successful, found ${actions.length} actions` + ); return actions; } catch (observeError) { - console.error('Error during observation:', observeError); + console.error("Error during observation:", observeError); throw observeError; } } - + return []; } catch (pageError) { - console.error('Error in page operation:', pageError); + console.error("Error in page operation:", pageError); throw pageError; } } catch (error: any) { @@ -251,17 +284,19 @@ const performWebObservation = async (url?: string, instruction?: string) => { }; const performWebExtraction = async ( - url?: string, - instruction?: string, + url?: string, + instruction?: string, schemaObj?: Record, useTextExtract?: boolean ) => { - console.log(`Starting extraction${url ? ` for ${url}` : ''} with instruction: ${instruction}`); - + console.log( + `Starting extraction${url ? ` for ${url}` : ""} with instruction: ${instruction}` + ); + try { const stagehand = await sessionManager.ensureStagehand(); const page = stagehand.page; - + try { // Navigate to the URL if provided if (url) { @@ -269,34 +304,34 @@ const performWebExtraction = async ( await page.goto(url); console.log(`Successfully navigated to ${url}`); } - + // Extract data if (instruction) { console.log(`Extracting with instruction: ${instruction}`); - + // Create a default schema if none is provided from Mastra Agent - const finalSchemaObj = schemaObj || { content: z.string() }; - + const finalSchemaObj = schemaObj || { content: z.string() }; + try { const schema = z.object(finalSchemaObj); - + const result = await page.extract({ instruction, schema, - useTextExtract + useTextExtract, }); - + console.log(`Extraction successful:`, result); return result; } catch (extractError) { - console.error('Error during extraction:', extractError); + console.error("Error during extraction:", extractError); throw extractError; } } - + return null; } catch (pageError) { - console.error('Error in page operation:', pageError); + console.error("Error in page operation:", pageError); throw pageError; } } catch (error: any) { @@ -308,10 +343,10 @@ const performWebExtraction = async ( // Add a navigation tool for convenience export const stagehandNavigateTool = createTool({ - id: 'web-navigate', - description: 'Navigate to a URL in the browser', + id: "web-navigate", + description: "Navigate to a URL in the browser", inputSchema: z.object({ - url: z.string().describe('URL to navigate to'), + url: z.string().describe("URL to navigate to"), }), outputSchema: z.object({ success: z.boolean(), @@ -321,23 +356,25 @@ export const stagehandNavigateTool = createTool({ execute: async ({ context }) => { try { const stagehand = await sessionManager.ensureStagehand(); - + // Navigate to the URL await stagehand.page.goto(context.url); - + // Get page title and current URL const title = await stagehand.page.evaluate(() => document.title); - const currentUrl = await stagehand.page.evaluate(() => window.location.href); - - return { - success: true, - title, - currentUrl + const currentUrl = await stagehand.page.evaluate( + () => window.location.href + ); + + return { + success: true, + title, + currentUrl, }; } catch (error: any) { return { success: false, - message: `Navigation failed: ${error.message}` + message: `Navigation failed: ${error.message}`, }; } }, diff --git a/examples/integrations/mastra/tsconfig.json b/examples/integrations/mastra/tsconfig.json index 910778e..c1841b3 100644 --- a/examples/integrations/mastra/tsconfig.json +++ b/examples/integrations/mastra/tsconfig.json @@ -9,12 +9,6 @@ "skipLibCheck": true, "outDir": "dist" }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "dist", - ".mastra" - ] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", ".mastra"] } diff --git a/examples/integrations/mongodb/README.md b/examples/integrations/mongodb/README.md index df318f8..cc0eadf 100644 --- a/examples/integrations/mongodb/README.md +++ b/examples/integrations/mongodb/README.md @@ -20,12 +20,14 @@ A web scraping project that uses Stagehand to extract structured data from e-com ## Installation 1. Clone the repository: + ``` git clone cd stagehand-mongodb-scraper ``` 2. Install dependencies: + ``` npm install ``` @@ -40,11 +42,13 @@ A web scraping project that uses Stagehand to extract structured data from e-com ## Usage 1. Start MongoDB locally: + ``` mongod ``` 2. Run the scraper: + ``` npm start ``` diff --git a/examples/integrations/mongodb/index.ts b/examples/integrations/mongodb/index.ts index eb78b9b..3a09d58 100644 --- a/examples/integrations/mongodb/index.ts +++ b/examples/integrations/mongodb/index.ts @@ -4,7 +4,7 @@ import chalk from "chalk"; import boxen from "boxen"; import { z } from "zod"; import { MongoServerError } from "mongodb"; -import { MongoClient, Db, Document } from 'mongodb'; +import { MongoClient, Db, Document } from "mongodb"; /** * 🤘 Welcome to Stagehand! Thanks so much for trying us out! @@ -23,8 +23,8 @@ import { MongoClient, Db, Document } from 'mongodb'; */ // ========== MongoDB Connection Configuration ========== -const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017'; -const DB_NAME = process.env.DB_NAME || 'scraper_db'; +const MONGO_URI = process.env.MONGO_URI || "mongodb://localhost:27017"; +const DB_NAME = process.env.DB_NAME || "scraper_db"; let client: MongoClient | null = null; let db: Db | null = null; @@ -43,7 +43,7 @@ const ProductSchema = z.object({ imageUrl: z.string().optional(), reviewCount: z.number().optional(), description: z.string().optional(), - specs: z.record(z.any()).optional() + specs: z.record(z.any()).optional(), }) satisfies z.ZodType; // Product list schema for results from category pages @@ -53,7 +53,7 @@ const ProductListSchema = z.object({ dateScraped: z.date(), totalProducts: z.number().optional(), page: z.number().optional(), - websiteName: z.string().optional() + websiteName: z.string().optional(), }) satisfies z.ZodType; // Types are inferred from the schemas @@ -62,8 +62,8 @@ export type ProductList = z.infer; // Collection names for MongoDB const COLLECTIONS = { - PRODUCTS: 'products', - PRODUCT_LISTS: 'product_lists' + PRODUCTS: "products", + PRODUCT_LISTS: "product_lists", } as const; // Index definitions for MongoDB collections @@ -75,21 +75,21 @@ interface IndexDefinition { const INDEXES = { [COLLECTIONS.PRODUCTS]: [ - { key: { rating: 1 }, name: 'rating_idx' } as IndexDefinition, - { key: { category: 1 }, name: 'category_idx' } as IndexDefinition, - { key: { url: 1 }, name: 'url_idx', unique: true } as IndexDefinition, - { key: { dateScraped: -1 }, name: 'dateScraped_idx' } as IndexDefinition + { key: { rating: 1 }, name: "rating_idx" } as IndexDefinition, + { key: { category: 1 }, name: "category_idx" } as IndexDefinition, + { key: { url: 1 }, name: "url_idx", unique: true } as IndexDefinition, + { key: { dateScraped: -1 }, name: "dateScraped_idx" } as IndexDefinition, ], [COLLECTIONS.PRODUCT_LISTS]: [ - { key: { category: 1 }, name: 'category_idx' } as IndexDefinition, - { key: { dateScraped: -1 }, name: 'dateScraped_idx' } as IndexDefinition - ] + { key: { category: 1 }, name: "category_idx" } as IndexDefinition, + { key: { dateScraped: -1 }, name: "dateScraped_idx" } as IndexDefinition, + ], } as const; // Check and create indexes for all collections async function createIndexes(db: Db): Promise { - console.log(chalk.blue('⚙️ Starting index creation...')); - + console.log(chalk.blue("⚙️ Starting index creation...")); + // First create all collections if they don't exist for (const collectionName of Object.keys(INDEXES)) { try { @@ -97,9 +97,14 @@ async function createIndexes(db: Db): Promise { console.log(chalk.green(`✅ Created collection: ${collectionName}`)); } catch (error) { if (error instanceof MongoServerError && error.code === 48) { - console.log(chalk.yellow(`⚠️ Collection ${collectionName} already exists`)); + console.log( + chalk.yellow(`⚠️ Collection ${collectionName} already exists`) + ); } else { - console.error(chalk.red(`❌ Error creating collection ${collectionName}:`), error); + console.error( + chalk.red(`❌ Error creating collection ${collectionName}:`), + error + ); throw error; } } @@ -107,31 +112,51 @@ async function createIndexes(db: Db): Promise { // Now create indexes for each collection for (const [collectionName, indexes] of Object.entries(INDEXES)) { - console.log(chalk.blue(`⚙️ Processing indexes for collection: ${collectionName}`)); + console.log( + chalk.blue(`⚙️ Processing indexes for collection: ${collectionName}`) + ); const collection = db.collection(collectionName); - + for (const index of indexes) { try { - console.log(chalk.blue(`⚙️ Creating index ${index.name} on ${collectionName} with keys:`, index.key)); + console.log( + chalk.blue( + `⚙️ Creating index ${index.name} on ${collectionName} with keys:`, + index.key + ) + ); const existingIndexes = await collection.listIndexes().toArray(); - const indexExists = existingIndexes.some(idx => idx.name === index.name); - + const indexExists = existingIndexes.some( + idx => idx.name === index.name + ); + if (indexExists) { - console.log(chalk.yellow(`⚠️ Index ${index.name} already exists on ${collectionName}`)); + console.log( + chalk.yellow( + `⚠️ Index ${index.name} already exists on ${collectionName}` + ) + ); } else { await collection.createIndex(index.key, { name: index.name, unique: index.unique || false, - background: false + background: false, }); - console.log(chalk.green(`✅ Created index ${index.name} on ${collectionName}`)); + console.log( + chalk.green(`✅ Created index ${index.name} on ${collectionName}`) + ); } } catch (error) { - console.error(chalk.red(`❌ Error creating index ${index.name} on ${collectionName}:`), error); + console.error( + chalk.red( + `❌ Error creating index ${index.name} on ${collectionName}:` + ), + error + ); } } } - + // Verify indexes were created for (const [collectionName, indexes] of Object.entries(INDEXES)) { const collection = db.collection(collectionName); @@ -139,8 +164,8 @@ async function createIndexes(db: Db): Promise { console.log(chalk.blue(`Indexes for ${collectionName}:`)); console.log(existingIndexes); } - - console.log(chalk.green('✅ Index creation completed')); + + console.log(chalk.green("✅ Index creation completed")); } // ========== MongoDB Utility Functions ========== @@ -149,22 +174,25 @@ async function createIndexes(db: Db): Promise { */ async function connectToMongo(): Promise { if (client) { - console.log('Using existing MongoDB connection'); + console.log("Using existing MongoDB connection"); return client.db(DB_NAME); } try { - console.log('Connecting to MongoDB...'); + console.log("Connecting to MongoDB..."); client = new MongoClient(MONGO_URI); await client.connect(); - - console.log('Connected to MongoDB'); - + + console.log("Connected to MongoDB"); + // Verify if database exists - const adminDb = client.db('admin'); + const adminDb = client.db("admin"); const databases = await adminDb.admin().listDatabases(); - const dbExists = databases.databases?.some((db: { name: string }) => db.name === DB_NAME) ?? false; - + const dbExists = + databases.databases?.some( + (db: { name: string }) => db.name === DB_NAME + ) ?? false; + if (!dbExists) { console.log(chalk.blue(`⚙️ Creating database: ${DB_NAME}`)); // Create a collection to trigger database creation @@ -174,17 +202,17 @@ async function connectToMongo(): Promise { } else { console.log(chalk.yellow(`⚠️ Database ${DB_NAME} already exists`)); } - + const db = client.db(DB_NAME); - + // Create indexes for all collections - console.log('Creating indexes...'); + console.log("Creating indexes..."); await createIndexes(db); - console.log('Indexes created successfully'); - + console.log("Indexes created successfully"); + return db; } catch (error) { - console.error('Error connecting to MongoDB:', error); + console.error("Error connecting to MongoDB:", error); throw error; } } @@ -195,7 +223,7 @@ async function connectToMongo(): Promise { async function closeMongo(): Promise { if (client) { await client.close(); - console.log('MongoDB connection closed'); + console.log("MongoDB connection closed"); client = null; db = null; } @@ -204,24 +232,32 @@ async function closeMongo(): Promise { /** * Stores data in a MongoDB collection */ -async function storeData(collectionName: string, data: T | T[]): Promise { +async function storeData( + collectionName: string, + data: T | T[] +): Promise { const db = await connectToMongo(); - + // Ensure collection exists try { await db.createCollection(collectionName); console.log(chalk.green(`✅ Created collection: ${collectionName}`)); } catch (error) { if (error instanceof MongoServerError && error.code === 48) { - console.log(chalk.yellow(`⚠️ Collection ${collectionName} already exists`)); + console.log( + chalk.yellow(`⚠️ Collection ${collectionName} already exists`) + ); } else { - console.error(chalk.red(`❌ Error creating collection ${collectionName}:`), error); + console.error( + chalk.red(`❌ Error creating collection ${collectionName}:`), + error + ); throw error; } } - + const collection = db.collection(collectionName); - + try { if (Array.isArray(data)) { await collection.insertMany(data as Document[]); @@ -230,7 +266,10 @@ async function storeData(collectionName: string, data: T | T } console.log(chalk.green(`✅ Stored data in ${collectionName}`)); } catch (error) { - console.error(chalk.red(`❌ Error storing data in ${collectionName}:`), error); + console.error( + chalk.red(`❌ Error storing data in ${collectionName}:`), + error + ); throw error; } } @@ -241,7 +280,7 @@ async function storeData(collectionName: string, data: T | T async function findData(collectionName: string, query = {}): Promise { const database = await connectToMongo(); const collection = database.collection(collectionName); - + try { const documents = await collection.find(query).toArray(); return documents as T[]; @@ -255,12 +294,12 @@ async function findData(collectionName: string, query = {}): Promise { * Aggregates data in a MongoDB collection */ async function aggregateData( - collectionName: string, + collectionName: string, pipeline: object[] ): Promise { const database = await connectToMongo(); const collection = database.collection(collectionName); - + try { const results = await collection.aggregate(pipeline).toArray(); return results as T[]; @@ -274,16 +313,21 @@ async function aggregateData( /** * Scrapes a product list from an Amazon category page */ -async function scrapeProductList(page: Page, categoryUrl: string): Promise { +async function scrapeProductList( + page: Page, + categoryUrl: string +): Promise { // Navigate to Amazon homepage first - await page.goto('https://www.amazon.com'); + await page.goto("https://www.amazon.com"); await page.waitForTimeout(2000); - + // Then navigate to the category page await page.goto(categoryUrl); - + // Wait for products to load - await page.waitForSelector('[data-component-type="s-search-result"]', { timeout: 10000 }); + await page.waitForSelector('[data-component-type="s-search-result"]', { + timeout: 10000, + }); await page.waitForTimeout(2000); // Scroll to load more products @@ -298,15 +342,18 @@ async function scrapeProductList(page: Page, categoryUrl: string): Promise { +async function scrapeProductDetails( + page: Page, + productUrl: string +): Promise { await page.goto(productUrl); - + // Wait for the page to load await page.waitForTimeout(2000); @@ -349,13 +399,14 @@ async function scrapeProductDetails(page: Page, productUrl: string): Promise { - window.scrollTo(0, document.body.scrollHeight * 2 / 3); + window.scrollTo(0, (document.body.scrollHeight * 2) / 3); }); await page.waitForTimeout(1000); // Extract product details using Stagehand const product = await page.extract({ - instruction: "Extract detailed product information from this Amazon product page, including name, price, description, specifications, brand, category, image URL, rating, review count, and availability", + instruction: + "Extract detailed product information from this Amazon product page, including name, price, description, specifications, brand, category, image URL, rating, review count, and availability", schema: ProductSchema.omit({ dateScraped: true }), }); @@ -390,7 +441,7 @@ async function runQueries(): Promise { // 1. Get total counts for each collection using MongoDB's native countDocuments console.log(chalk.yellow("\n📊 Collection Counts:")); - if (!db) throw new Error('MongoDB connection not established'); + if (!db) throw new Error("MongoDB connection not established"); for (const [name, collection] of Object.entries(COLLECTIONS)) { const count = await db.collection(collection).countDocuments(); console.log(`${chalk.green(name)}: ${count} documents`); @@ -402,40 +453,43 @@ async function runQueries(): Promise { COLLECTIONS.PRODUCTS, [ { $group: { _id: "$category", count: { $sum: 1 } } }, - { $sort: { count: -1 } } + { $sort: { count: -1 } }, ] ); - + console.table( productsByCategory.map(item => ({ Category: item._id || "Unknown", - Count: item.count + Count: item.count, })) ); // 3. Find highest rated products console.log(chalk.yellow("\n📊 Top Rated Products:")); // First get the count of highly rated products - if (!db) throw new Error('MongoDB connection not established'); - const count = await db.collection(COLLECTIONS.PRODUCTS).countDocuments({ rating: { $gte: 4 } }); - console.log(chalk.yellow(`Found ${count} highly rated products (4+ stars)`)); + if (!db) throw new Error("MongoDB connection not established"); + const count = await db + .collection(COLLECTIONS.PRODUCTS) + .countDocuments({ rating: { $gte: 4 } }); + console.log( + chalk.yellow(`Found ${count} highly rated products (4+ stars)`) + ); // Only fetch and display the products if there are any if (count > 0) { - const highestRatedProducts = await findData( - COLLECTIONS.PRODUCTS, - { rating: { $gte: 4 } } - ); + const highestRatedProducts = await findData(COLLECTIONS.PRODUCTS, { + rating: { $gte: 4 }, + }); console.table( highestRatedProducts.map((product: any) => ({ Name: product.name, Price: product.price, Rating: product.rating, - Category: product.category || "Unknown" + Category: product.category || "Unknown", })) ); } - + console.log(chalk.green("\n✅ Queries completed successfully!")); } catch (error) { console.error(chalk.red("❌ Error running queries:"), error); @@ -455,47 +509,64 @@ async function main({ try { // Connect to MongoDB const db = await connectToMongo(); - + // Verify indexes were created - console.log(chalk.blue('Verifying indexes...')); + console.log(chalk.blue("Verifying indexes...")); for (const [collectionName, indexes] of Object.entries(INDEXES)) { const collection = db.collection(collectionName); const existingIndexes = await collection.listIndexes().toArray(); console.log(chalk.blue(`Indexes for ${collectionName}:`)); console.log(existingIndexes); } - + // Define the category URL for Amazon electronics const categoryUrl = "https://www.amazon.com/s?k=laptops"; - + console.log(chalk.blue("📊 Starting to scrape product listing...")); - + // Scrape product listing const productList = await scrapeProductList(page, categoryUrl); - console.log(chalk.green(`✅ Scraped ${productList.products.length} products from category: ${productList.category}`)); - + console.log( + chalk.green( + `✅ Scraped ${productList.products.length} products from category: ${productList.category}` + ) + ); + // Scrape detailed information for the first 3 products const productsToScrape = productList.products.slice(0, 3); - + for (const [index, product] of productsToScrape.entries()) { - console.log(chalk.blue(`📊 Scraping details for product ${index + 1}/${productsToScrape.length}: ${product.name}`)); - + console.log( + chalk.blue( + `📊 Scraping details for product ${index + 1}/${productsToScrape.length}: ${product.name}` + ) + ); + try { // Scrape product details const detailedProduct = await scrapeProductDetails(page, product.url); - console.log(chalk.green(`✅ Scraped detailed information for: ${detailedProduct.name}`)); - + console.log( + chalk.green( + `✅ Scraped detailed information for: ${detailedProduct.name}` + ) + ); + // Wait between requests to avoid rate limiting await page.waitForTimeout(2000); } catch (error) { - console.error(chalk.red(`❌ Error scraping product ${product.name}:`), error); + console.error( + chalk.red(`❌ Error scraping product ${product.name}:`), + error + ); } } - + // Run queries on the collected data await runQueries(); - - console.log(chalk.green("🎉 Scraping and MongoDB operations completed successfully!")); + + console.log( + chalk.green("🎉 Scraping and MongoDB operations completed successfully!") + ); } catch (error) { console.error(chalk.red("❌ Error during scraping:"), error); } finally { @@ -515,31 +586,31 @@ async function run() { console.log( boxen( `View this session live in your browser: \n${chalk.blue( - `https://browserbase.com/sessions/${stagehand.browserbaseSessionID}`, + `https://browserbase.com/sessions/${stagehand.browserbaseSessionID}` )}`, { title: "Browserbase", padding: 1, margin: 3, - }, - ), + } + ) ); } const page = stagehand.page; const context = stagehand.context; - + await main({ page, context, stagehand, }); - + await stagehand.close(); console.log( `\n🤘 Thanks so much for using Stagehand! Reach out to us on Slack if you have any feedback: ${chalk.blue( - "https://stagehand.dev/slack", - )}\n`, + "https://stagehand.dev/slack" + )}\n` ); } diff --git a/examples/integrations/mongodb/utils.ts b/examples/integrations/mongodb/utils.ts index 182b040..0118136 100644 --- a/examples/integrations/mongodb/utils.ts +++ b/examples/integrations/mongodb/utils.ts @@ -10,7 +10,7 @@ export function announce(message: string, title?: string) { padding: 1, margin: 3, title: title || "Stagehand", - }), + }) ); } @@ -44,13 +44,13 @@ export function validateZodSchema(schema: z.ZodTypeAny, data: unknown) { export async function drawObserveOverlay(page: Page, results: ObserveResult[]) { // Convert single xpath to array for consistent handling - const xpathList = results.map((result) => result.selector); + const xpathList = results.map(result => result.selector); // Filter out empty xpaths - const validXpaths = xpathList.filter((xpath) => xpath !== "xpath="); + const validXpaths = xpathList.filter(xpath => xpath !== "xpath="); - await page.evaluate((selectors) => { - selectors.forEach((selector) => { + await page.evaluate(selectors => { + selectors.forEach(selector => { let element; if (selector.startsWith("xpath=")) { const xpath = selector.substring(6); @@ -59,7 +59,7 @@ export async function drawObserveOverlay(page: Page, results: ObserveResult[]) { document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, - null, + null ).singleNodeValue; } else { element = document.querySelector(selector); @@ -87,7 +87,7 @@ export async function clearOverlays(page: Page) { // remove existing stagehandObserve attributes await page.evaluate(() => { const elements = document.querySelectorAll('[stagehandObserve="true"]'); - elements.forEach((el) => { + elements.forEach(el => { const parent = el.parentNode; while (el.firstChild) { parent?.insertBefore(el.firstChild, el); @@ -99,7 +99,7 @@ export async function clearOverlays(page: Page) { export async function simpleCache( instruction: string, - actionToCache: ObserveResult, + actionToCache: ObserveResult ) { // Save action to cache.json try { @@ -123,7 +123,7 @@ export async function simpleCache( } export async function readCache( - instruction: string, + instruction: string ): Promise { try { const existingCache = await fs.readFile("cache.json", "utf-8"); @@ -143,7 +143,7 @@ export async function readCache( */ export async function actWithCache( page: Page, - instruction: string, + instruction: string ): Promise { // Try to get action from cache first const cachedAction = await readCache(instruction); diff --git a/examples/integrations/portia/README.md b/examples/integrations/portia/README.md index 70c5179..a048ac5 100644 --- a/examples/integrations/portia/README.md +++ b/examples/integrations/portia/README.md @@ -17,7 +17,7 @@ Portia incorporates the concept of end users with the [`EndUser`](https://docs.p Here are some examples of the kinds of queries that can be handled in 20 lines of code with the Portia / Browserbase integration: - Send a message to Bob Smith on LinkedIn asking him if he's free on Tuesday for a meeting -- Get my Google Doc shopping list and add all items in it to my shopping trolley on the Walmart website +- Get my Google Doc shopping list and add all items in it to my shopping trolley on the Walmart website - Book me unto the 8am hot yoga class - Star a GitHub repository after authenticating - Extract data from authenticated dashboards diff --git a/examples/integrations/stripe/README.md b/examples/integrations/stripe/README.md index 35dd3aa..3671851 100644 --- a/examples/integrations/stripe/README.md +++ b/examples/integrations/stripe/README.md @@ -1,12 +1,13 @@ # 🚀 Agentic Credit Card Automation -Effortlessly create virtual cards with **Stripe** and automate purchases using **Browserbase**. +Effortlessly create virtual cards with **Stripe** and automate purchases using **Browserbase**. Find the full documentation here: https://docs.browserbase.com/integrations/stripe/introduction ## 📌 Overview This project enables you to: + - **Create virtual cards** with spending controls using Stripe Issuing - **Retrieve virtual card details** - **Automate online purchases** @@ -17,8 +18,8 @@ For full setup instructions, API details, and code examples, visit the official 📄 **[Agentic Credit Card Automation Docs](https://docs.browserbase.com/integrations/stripe/introduction)** - # 📂 Structure + ``` stripe/ │── node/ # Node.js implementation diff --git a/examples/integrations/stripe/node/1-create-cardholder.ts b/examples/integrations/stripe/node/1-create-cardholder.ts index 1dd5bec..e7ed47a 100644 --- a/examples/integrations/stripe/node/1-create-cardholder.ts +++ b/examples/integrations/stripe/node/1-create-cardholder.ts @@ -1,28 +1,28 @@ -import Stripe from 'stripe'; -import dotenv from 'dotenv'; +import Stripe from "stripe"; +import dotenv from "dotenv"; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_API_KEY!); async function createCardholder() { - const cardholder = await stripe.issuing.cardholders.create({ - name: "Browserbase User", - email: "hello@browserbase.com", - phone_number: "+15555555555", - status: 'active', - type: 'individual', - billing: { - address: { - line1: '123 Main Street', - city: 'San Francisco', - state: 'CA', - country: 'US', - postal_code: '94111', - } - }, - }); - console.log("Cardholder created:", cardholder.id); - return cardholder; + const cardholder = await stripe.issuing.cardholders.create({ + name: "Browserbase User", + email: "hello@browserbase.com", + phone_number: "+15555555555", + status: "active", + type: "individual", + billing: { + address: { + line1: "123 Main Street", + city: "San Francisco", + state: "CA", + country: "US", + postal_code: "94111", + }, + }, + }); + console.log("Cardholder created:", cardholder.id); + return cardholder; } -const cardholder = createCardholder(); \ No newline at end of file +const cardholder = createCardholder(); diff --git a/examples/integrations/stripe/node/2-create-card.ts b/examples/integrations/stripe/node/2-create-card.ts index 65731ea..67b38eb 100644 --- a/examples/integrations/stripe/node/2-create-card.ts +++ b/examples/integrations/stripe/node/2-create-card.ts @@ -1,27 +1,31 @@ -import Stripe from 'stripe'; -import dotenv from 'dotenv'; +import Stripe from "stripe"; +import dotenv from "dotenv"; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_API_KEY!); async function createCard(cardholderId: string) { - const card = await stripe.issuing.cards.create({ - cardholder: cardholderId, - currency: 'usd', - type: 'virtual', - spending_controls: { - allowed_categories: ['charitable_and_social_service_organizations_fundraising'], - // Choose to block certain categories instead of allowing them - // blocked_categories: ['automated_cash_disburse'], - spending_limits: [{ - amount: 7500, // $75.00 measured in cents - interval: 'daily', // all_time, daily, weekly, monthly, yearly, per_authorization - }], + const card = await stripe.issuing.cards.create({ + cardholder: cardholderId, + currency: "usd", + type: "virtual", + spending_controls: { + allowed_categories: [ + "charitable_and_social_service_organizations_fundraising", + ], + // Choose to block certain categories instead of allowing them + // blocked_categories: ['automated_cash_disburse'], + spending_limits: [ + { + amount: 7500, // $75.00 measured in cents + interval: "daily", // all_time, daily, weekly, monthly, yearly, per_authorization }, - }); + ], + }, + }); - console.log('Card created:', card.id); - return card; + console.log("Card created:", card.id); + return card; } const cardholderId = "ich_INPUT_CARDHOLDER_ID_HERE"; // replace with your cardholder id from the previous step -const virtual_card = createCard(cardholderId); \ No newline at end of file +const virtual_card = createCard(cardholderId); diff --git a/examples/integrations/stripe/node/3-get-card.ts b/examples/integrations/stripe/node/3-get-card.ts index 0aac123..c49c28a 100644 --- a/examples/integrations/stripe/node/3-get-card.ts +++ b/examples/integrations/stripe/node/3-get-card.ts @@ -1,28 +1,29 @@ -import Stripe from 'stripe'; -import dotenv from 'dotenv'; +import Stripe from "stripe"; +import dotenv from "dotenv"; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_API_KEY!); export async function getCard(cardId: string) { - const card = await stripe.issuing.cards.retrieve( - cardId, {expand: ['number', 'cvc']}); + const card = await stripe.issuing.cards.retrieve(cardId, { + expand: ["number", "cvc"], + }); - const cardInfo = { - cardholder_firstName: card.cardholder.name.split(' ')[0], - cardholder_lastName: card.cardholder.name.split(' ')[1], - cardholder_email: card.cardholder.email, - cardholder_phone: card.cardholder.phone_number, - cardholder_address: card.cardholder.billing.address, - card_number: card.number, - expiration_month: card.exp_month, - expiration_year: card.exp_year.toString().slice(-2), // 2028 -> 28 - cvc: card.cvc, - brand: card.brand, - currency: card.currency, - }; - console.log('Card info:', cardInfo); - return cardInfo; + const cardInfo = { + cardholder_firstName: card.cardholder.name.split(" ")[0], + cardholder_lastName: card.cardholder.name.split(" ")[1], + cardholder_email: card.cardholder.email, + cardholder_phone: card.cardholder.phone_number, + cardholder_address: card.cardholder.billing.address, + card_number: card.number, + expiration_month: card.exp_month, + expiration_year: card.exp_year.toString().slice(-2), // 2028 -> 28 + cvc: card.cvc, + brand: card.brand, + currency: card.currency, + }; + console.log("Card info:", cardInfo); + return cardInfo; } const cardId = "ic_INPUT_CARD_ID_HERE"; // replace with your card id from the previous step -getCard(cardId); \ No newline at end of file +getCard(cardId); diff --git a/examples/integrations/stripe/node/4-make-payment.ts b/examples/integrations/stripe/node/4-make-payment.ts index aa38bcb..18cd290 100644 --- a/examples/integrations/stripe/node/4-make-payment.ts +++ b/examples/integrations/stripe/node/4-make-payment.ts @@ -1,8 +1,8 @@ -import dotenv from 'dotenv'; +import dotenv from "dotenv"; dotenv.config(); import { chromium } from "playwright-core"; import Browserbase from "@browserbasehq/sdk/index.mjs"; -import { getCard } from './3-get-card'; +import { getCard } from "./3-get-card"; const BROWSERBASE_API_KEY = process.env.BROWSERBASE_API_KEY!; const BROWSERBASE_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID!; @@ -10,52 +10,78 @@ const BROWSERBASE_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID!; const cardId = "ic_INPUT_CARD_ID_HERE"; // replace with your card id from the previous step (async () => { - const bb = new Browserbase({apiKey: BROWSERBASE_API_KEY}); - const session = await bb.sessions.create({ - projectId: BROWSERBASE_PROJECT_ID, - }); - const browser = await chromium.connectOverCDP(session.connectUrl); - const defaultContext = browser.contexts()[0]; - const page = defaultContext.pages()[0]; - - const paymentInfo = await getCard(cardId); - - // log session ID url - console.log(`Session URL: https://www.browserbase.com/sessions/${session.id}`); - - // go to the donation page - await page?.goto("https://www.redcross.org/donate/donation.html"); - - // click the first donation amount - await page?.click("#modf-handle-0-radio"); - - // click the continue button - await page?.click("text=Continue"); - - // click the credit card button - await page?.click("text=credit card"); - - // Fill billing information - await page?.fill("input[name='bill_to_forename']", paymentInfo.cardholder_firstName!); - await page?.fill("input[name='bill_to_surname']", paymentInfo.cardholder_lastName!); - await page?.fill("input[name='bill_to_email']", paymentInfo.cardholder_email!); - await page?.fill("input[name='bill_to_phone']", paymentInfo.cardholder_phone!); - - // Fill in the address information - await page?.fill("input[name='bill_to_address_line1']", paymentInfo.cardholder_address.line1!); - await page?.fill("input[name='bill_to_address_city']", paymentInfo.cardholder_address.city!); - await page?.fill("input[name='bill_to_address_postal_code']", paymentInfo.cardholder_address.postal_code!); - await page?.selectOption("select#bill_to_address_state", paymentInfo.cardholder_address.state!); - - // Fill in the card information - await page?.fill("input#cardnumber", paymentInfo.card_number!); - await page?.fill("input#MM", paymentInfo.expiration_month!.toString()); - await page?.fill("input#YY", paymentInfo.expiration_year!.toString()); - await page?.fill("input#CVC", paymentInfo.cvc!.toString()); - - // click donate button - await page?.click("text=Donate"); - - await page.close(); - await browser.close(); -})().catch((error) => console.error(error.message)); \ No newline at end of file + const bb = new Browserbase({ apiKey: BROWSERBASE_API_KEY }); + const session = await bb.sessions.create({ + projectId: BROWSERBASE_PROJECT_ID, + }); + const browser = await chromium.connectOverCDP(session.connectUrl); + const defaultContext = browser.contexts()[0]; + const page = defaultContext.pages()[0]; + + const paymentInfo = await getCard(cardId); + + // log session ID url + console.log( + `Session URL: https://www.browserbase.com/sessions/${session.id}` + ); + + // go to the donation page + await page?.goto("https://www.redcross.org/donate/donation.html"); + + // click the first donation amount + await page?.click("#modf-handle-0-radio"); + + // click the continue button + await page?.click("text=Continue"); + + // click the credit card button + await page?.click("text=credit card"); + + // Fill billing information + await page?.fill( + "input[name='bill_to_forename']", + paymentInfo.cardholder_firstName! + ); + await page?.fill( + "input[name='bill_to_surname']", + paymentInfo.cardholder_lastName! + ); + await page?.fill( + "input[name='bill_to_email']", + paymentInfo.cardholder_email! + ); + await page?.fill( + "input[name='bill_to_phone']", + paymentInfo.cardholder_phone! + ); + + // Fill in the address information + await page?.fill( + "input[name='bill_to_address_line1']", + paymentInfo.cardholder_address.line1! + ); + await page?.fill( + "input[name='bill_to_address_city']", + paymentInfo.cardholder_address.city! + ); + await page?.fill( + "input[name='bill_to_address_postal_code']", + paymentInfo.cardholder_address.postal_code! + ); + await page?.selectOption( + "select#bill_to_address_state", + paymentInfo.cardholder_address.state! + ); + + // Fill in the card information + await page?.fill("input#cardnumber", paymentInfo.card_number!); + await page?.fill("input#MM", paymentInfo.expiration_month!.toString()); + await page?.fill("input#YY", paymentInfo.expiration_year!.toString()); + await page?.fill("input#CVC", paymentInfo.cvc!.toString()); + + // click donate button + await page?.click("text=Donate"); + + await page.close(); + await browser.close(); +})().catch(error => console.error(error.message)); diff --git a/examples/integrations/stripe/node/README.md b/examples/integrations/stripe/node/README.md index c3b2b00..f2eb1fd 100644 --- a/examples/integrations/stripe/node/README.md +++ b/examples/integrations/stripe/node/README.md @@ -5,6 +5,7 @@ Effortlessly create virtual cards with **Stripe** and automate purchases using * ## 📌 Overview This Node.js implementation enables you to: + - **Create virtual cards** with spending controls using Stripe Issuing - **Retrieve virtual card details** securely - **Automate online purchases** with Playwright and Browserbase @@ -12,7 +13,8 @@ This Node.js implementation enables you to: ## 🛠️ Setup ### Prerequisites -- Node.js 18+ + +- Node.js 18+ - npm or yarn - Stripe account with Issuing enabled - Browserbase account @@ -20,12 +22,14 @@ This Node.js implementation enables you to: ### Installation 1. **Install dependencies**: + ```bash npm install ``` 2. **Configure environment variables**: -Create a `.env` file in this directory: + Create a `.env` file in this directory: + ```env STRIPE_API_KEY=sk_test_your_stripe_secret_key BROWSERBASE_API_KEY=your_browserbase_api_key @@ -33,6 +37,7 @@ BROWSERBASE_PROJECT_ID=your_browserbase_project_id ``` 3. **Install Playwright browsers**: + ```bash npm run postinstall ``` @@ -40,49 +45,60 @@ npm run postinstall ## 🚀 Usage ### Step 1: Create a Cardholder + ```bash npx tsx 1-create-cardholder.ts ``` ### Step 2: Create a Virtual Card + ```bash npx tsx 2-create-card.ts ``` + ⚠️ **Important**: Update the `cardholderId` variable with the ID from Step 1 ### Step 3: Retrieve Card Details + ```bash npx tsx 3-get-card.ts ``` + ⚠️ **Important**: Update the `cardId` variable with the ID from Step 2 ### Step 4: Make an Automated Payment + ```bash npx tsx 4-make-payment.ts ``` + ⚠️ **Important**: Update the `cardId` variable with the ID from Step 2 ## 📁 Files -| File | Description | -|------|-------------| -| `1-create-cardholder.ts` | Creates a Stripe cardholder with billing information | -| `2-create-card.ts` | Creates a virtual card with spending limits | -| `3-get-card.ts` | Retrieves card details including sensitive information | -| `4-make-payment.ts` | Automates a donation using Playwright and Browserbase | -| `package.json` | Node.js dependencies and scripts | -| `.env` | Environment variables (not tracked in git) | +| File | Description | +| ------------------------ | ------------------------------------------------------ | +| `1-create-cardholder.ts` | Creates a Stripe cardholder with billing information | +| `2-create-card.ts` | Creates a virtual card with spending limits | +| `3-get-card.ts` | Retrieves card details including sensitive information | +| `4-make-payment.ts` | Automates a donation using Playwright and Browserbase | +| `package.json` | Node.js dependencies and scripts | +| `.env` | Environment variables (not tracked in git) | ## 🔧 Configuration ### Spending Controls + Edit `2-create-card.ts` to customize: + - **Allowed categories**: Restrict card usage to specific merchant categories - **Spending limits**: Set daily/monthly/yearly limits - **Blocked categories**: Prevent usage at certain merchant types ### Payment Automation + Modify `4-make-payment.ts` to: + - Change the target website - Customize form filling logic - Add error handling and validation @@ -105,4 +121,4 @@ For full API documentation and advanced usage: - **Expense management**: Create cards for specific projects or employees - **Testing payments**: Automate payment flow testing - **Subscription management**: Programmatically manage recurring payments -- **Budget enforcement**: Set spending limits per card or category \ No newline at end of file +- **Budget enforcement**: Set spending limits per card or category diff --git a/examples/integrations/stripe/python/README.md b/examples/integrations/stripe/python/README.md index 2e66b48..3365958 100644 --- a/examples/integrations/stripe/python/README.md +++ b/examples/integrations/stripe/python/README.md @@ -5,6 +5,7 @@ Effortlessly create virtual cards with **Stripe** and automate purchases using * ## 📌 Overview This Python implementation enables you to: + - **Create virtual cards** with spending controls using Stripe Issuing - **Retrieve virtual card details** securely - **Automate online purchases** with Playwright and Browserbase @@ -12,6 +13,7 @@ This Python implementation enables you to: ## 🛠️ Setup ### Prerequisites + - Python 3.8+ - pip or poetry - Stripe account with Issuing enabled @@ -20,12 +22,14 @@ This Python implementation enables you to: ### Installation 1. **Install dependencies**: + ```bash pip install -r requirements.txt ``` 2. **Configure environment variables**: -Create a `.env` file in this directory: + Create a `.env` file in this directory: + ```env STRIPE_API_KEY=sk_test_your_stripe_secret_key BROWSERBASE_API_KEY=your_browserbase_api_key @@ -33,6 +37,7 @@ BROWSERBASE_PROJECT_ID=your_browserbase_project_id ``` 3. **Install Playwright browsers**: + ```bash playwright install ``` @@ -40,49 +45,60 @@ playwright install ## 🚀 Usage ### Step 1: Create a Cardholder + ```bash python create_cardholder.py ``` ### Step 2: Create a Virtual Card + ```bash python create_card.py ``` + ⚠️ **Important**: Update the `cardholder_id` variable with the ID from Step 1 ### Step 3: Retrieve Card Details + ```bash python get_card.py ``` + ⚠️ **Important**: Uncomment and update the `card_id` variable with the ID from Step 2 ### Step 4: Make an Automated Payment + ```bash python make-payment.py ``` + ⚠️ **Important**: Update the `cardId` variable with the ID from Step 2 ## 📁 Files -| File | Description | -|------|-------------| -| `create_cardholder.py` | Creates a Stripe cardholder with billing information | -| `create_card.py` | Creates a virtual card with spending limits | -| `get_card.py` | Retrieves card details including sensitive information | -| `make-payment.py` | Automates a donation using Playwright and Browserbase | -| `requirements.txt` | Python dependencies | -| `.env` | Environment variables (not tracked in git) | +| File | Description | +| ---------------------- | ------------------------------------------------------ | +| `create_cardholder.py` | Creates a Stripe cardholder with billing information | +| `create_card.py` | Creates a virtual card with spending limits | +| `get_card.py` | Retrieves card details including sensitive information | +| `make-payment.py` | Automates a donation using Playwright and Browserbase | +| `requirements.txt` | Python dependencies | +| `.env` | Environment variables (not tracked in git) | ## 🔧 Configuration ### Spending Controls + Edit `create_card.py` to customize: + - **Allowed categories**: Restrict card usage to specific merchant categories -- **Spending limits**: Set daily/monthly/yearly limits +- **Spending limits**: Set daily/monthly/yearly limits - **Blocked categories**: Prevent usage at certain merchant types ### Payment Automation + Modify `make-payment.py` to: + - Change the target website - Customize form filling logic - Add error handling and validation @@ -90,6 +106,7 @@ Modify `make-payment.py` to: ## 🐍 Python-Specific Features ### Virtual Environment (Recommended) + ```bash python -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate @@ -97,12 +114,15 @@ pip install -r requirements.txt ``` ### Error Handling + The Python implementation includes: + - Environment variable validation - Stripe API error handling - Playwright timeout management ### Dependencies + - **stripe**: Official Stripe Python library - **browserbasehq**: Browserbase SDK for Python - **playwright**: Browser automation framework @@ -133,7 +153,8 @@ For full API documentation and advanced usage: ## 🚨 Troubleshooting ### Common Issues + - **Import errors**: Ensure all dependencies are installed via `pip install -r requirements.txt` - **Playwright errors**: Run `playwright install` to download browser binaries - **Stripe API errors**: Verify your API keys and account permissions -- **Environment variables**: Double-check your `.env` file formatting \ No newline at end of file +- **Environment variables**: Double-check your `.env` file formatting diff --git a/examples/integrations/stripe/stagehand/1-create-cardholder.ts b/examples/integrations/stripe/stagehand/1-create-cardholder.ts index 1dd5bec..e7ed47a 100644 --- a/examples/integrations/stripe/stagehand/1-create-cardholder.ts +++ b/examples/integrations/stripe/stagehand/1-create-cardholder.ts @@ -1,28 +1,28 @@ -import Stripe from 'stripe'; -import dotenv from 'dotenv'; +import Stripe from "stripe"; +import dotenv from "dotenv"; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_API_KEY!); async function createCardholder() { - const cardholder = await stripe.issuing.cardholders.create({ - name: "Browserbase User", - email: "hello@browserbase.com", - phone_number: "+15555555555", - status: 'active', - type: 'individual', - billing: { - address: { - line1: '123 Main Street', - city: 'San Francisco', - state: 'CA', - country: 'US', - postal_code: '94111', - } - }, - }); - console.log("Cardholder created:", cardholder.id); - return cardholder; + const cardholder = await stripe.issuing.cardholders.create({ + name: "Browserbase User", + email: "hello@browserbase.com", + phone_number: "+15555555555", + status: "active", + type: "individual", + billing: { + address: { + line1: "123 Main Street", + city: "San Francisco", + state: "CA", + country: "US", + postal_code: "94111", + }, + }, + }); + console.log("Cardholder created:", cardholder.id); + return cardholder; } -const cardholder = createCardholder(); \ No newline at end of file +const cardholder = createCardholder(); diff --git a/examples/integrations/stripe/stagehand/2-create-card.ts b/examples/integrations/stripe/stagehand/2-create-card.ts index 65731ea..67b38eb 100644 --- a/examples/integrations/stripe/stagehand/2-create-card.ts +++ b/examples/integrations/stripe/stagehand/2-create-card.ts @@ -1,27 +1,31 @@ -import Stripe from 'stripe'; -import dotenv from 'dotenv'; +import Stripe from "stripe"; +import dotenv from "dotenv"; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_API_KEY!); async function createCard(cardholderId: string) { - const card = await stripe.issuing.cards.create({ - cardholder: cardholderId, - currency: 'usd', - type: 'virtual', - spending_controls: { - allowed_categories: ['charitable_and_social_service_organizations_fundraising'], - // Choose to block certain categories instead of allowing them - // blocked_categories: ['automated_cash_disburse'], - spending_limits: [{ - amount: 7500, // $75.00 measured in cents - interval: 'daily', // all_time, daily, weekly, monthly, yearly, per_authorization - }], + const card = await stripe.issuing.cards.create({ + cardholder: cardholderId, + currency: "usd", + type: "virtual", + spending_controls: { + allowed_categories: [ + "charitable_and_social_service_organizations_fundraising", + ], + // Choose to block certain categories instead of allowing them + // blocked_categories: ['automated_cash_disburse'], + spending_limits: [ + { + amount: 7500, // $75.00 measured in cents + interval: "daily", // all_time, daily, weekly, monthly, yearly, per_authorization }, - }); + ], + }, + }); - console.log('Card created:', card.id); - return card; + console.log("Card created:", card.id); + return card; } const cardholderId = "ich_INPUT_CARDHOLDER_ID_HERE"; // replace with your cardholder id from the previous step -const virtual_card = createCard(cardholderId); \ No newline at end of file +const virtual_card = createCard(cardholderId); diff --git a/examples/integrations/stripe/stagehand/3-get-card.ts b/examples/integrations/stripe/stagehand/3-get-card.ts index 14b931e..5e760a9 100644 --- a/examples/integrations/stripe/stagehand/3-get-card.ts +++ b/examples/integrations/stripe/stagehand/3-get-card.ts @@ -1,28 +1,29 @@ -import Stripe from 'stripe'; -import dotenv from 'dotenv'; +import Stripe from "stripe"; +import dotenv from "dotenv"; dotenv.config(); const stripe = new Stripe(process.env.STRIPE_API_KEY!); export async function getCard(cardId: string) { - const card = await stripe.issuing.cards.retrieve( - cardId, {expand: ['number', 'cvc']}); + const card = await stripe.issuing.cards.retrieve(cardId, { + expand: ["number", "cvc"], + }); - const cardInfo = { - cardholder_firstName: card.cardholder.name.split(' ')[0], - cardholder_lastName: card.cardholder.name.split(' ')[1], - cardholder_email: card.cardholder.email, - cardholder_phone: card.cardholder.phone_number, - cardholder_address: card.cardholder.billing.address, - card_number: card.number, - expiration_month: card.exp_month, - expiration_year: card.exp_year.toString().slice(-2), // 2028 -> 28 - cvc: card.cvc, - brand: card.brand, - currency: card.currency, - }; - console.log('Card info:', cardInfo); - return cardInfo; + const cardInfo = { + cardholder_firstName: card.cardholder.name.split(" ")[0], + cardholder_lastName: card.cardholder.name.split(" ")[1], + cardholder_email: card.cardholder.email, + cardholder_phone: card.cardholder.phone_number, + cardholder_address: card.cardholder.billing.address, + card_number: card.number, + expiration_month: card.exp_month, + expiration_year: card.exp_year.toString().slice(-2), // 2028 -> 28 + cvc: card.cvc, + brand: card.brand, + currency: card.currency, + }; + console.log("Card info:", cardInfo); + return cardInfo; } const cardId = "ic_INPUT_CARD_ID_HERE"; // replace with your card id from the previous step -// getCard(cardId); \ No newline at end of file +// getCard(cardId); diff --git a/examples/integrations/stripe/stagehand/4-make-payment.ts b/examples/integrations/stripe/stagehand/4-make-payment.ts index ea14eff..1a22a3f 100644 --- a/examples/integrations/stripe/stagehand/4-make-payment.ts +++ b/examples/integrations/stripe/stagehand/4-make-payment.ts @@ -34,42 +34,42 @@ export async function main({ const paymentInfo = await getCard(cardId); // Navigate to Red Cross donation page - await page.goto('https://www.redcross.org/donate/donation.html/') + await page.goto("https://www.redcross.org/donate/donation.html/"); const donationAmount = await page.observe({ - instruction: "Find the donation amounts", - returnAction: true, - onlyVisible: false, + instruction: "Find the donation amounts", + returnAction: true, + onlyVisible: false, }); // Click the first donation amount - await page.act(donationAmount[0]) + await page.act(donationAmount[0]); // Find the continue button and click it const continueButton = await page.observe({ - instruction: "Find the continue button and click it", - returnAction: true, - onlyVisible: false, + instruction: "Find the continue button and click it", + returnAction: true, + onlyVisible: false, }); - await page.act(continueButton[0]) + await page.act(continueButton[0]); // Find the credit card button and click it const creditCardButton = await page.observe({ - instruction: "Find the credit card button and click it", - returnAction: true, - onlyVisible: false, + instruction: "Find the credit card button and click it", + returnAction: true, + onlyVisible: false, }); - await page.act(creditCardButton[0]) + await page.act(creditCardButton[0]); - await page.act({action: "click the continue button"}) + await page.act({ action: "click the continue button" }); const formValues = await page.observe({ - instruction: `Fill in the form with the following values: ${JSON.stringify(paymentInfo)}`, - returnAction: true, - onlyVisible: false, + instruction: `Fill in the form with the following values: ${JSON.stringify(paymentInfo)}`, + returnAction: true, + onlyVisible: false, }); console.log("formValues", formValues); // Fill in the form with the values for (const value of formValues) { - await page.act(value); + await page.act(value); } } diff --git a/examples/integrations/stripe/stagehand/stagehand.config.ts b/examples/integrations/stripe/stagehand/stagehand.config.ts index 59a56e3..75ec84c 100644 --- a/examples/integrations/stripe/stagehand/stagehand.config.ts +++ b/examples/integrations/stripe/stagehand/stagehand.config.ts @@ -1,7 +1,7 @@ import type { ConstructorParams, LogLine } from "@browserbasehq/stagehand"; import dotenv from "dotenv"; -false -false +false; +false; dotenv.config(); @@ -21,11 +21,9 @@ const StagehandConfig: ConstructorParams = { browserbaseSessionID: undefined /* Session ID for resuming Browserbase sessions */, modelName: "gpt-4o" /* Name of the model to use */, - modelClientOptions: { - apiKey: process.env.OPENAI_API_KEY, - } /* Configuration options for the model client */, - - + modelClientOptions: { + apiKey: process.env.OPENAI_API_KEY, + } /* Configuration options for the model client */, }; export default StagehandConfig; @@ -78,4 +76,4 @@ export function logLineToString(logLine: LogLine): string { console.error(`Error logging line:`, error); return "error logging line"; } -} \ No newline at end of file +} diff --git a/examples/integrations/stripe/stagehand/utils.ts b/examples/integrations/stripe/stagehand/utils.ts index 2ef7c26..3844b78 100644 --- a/examples/integrations/stripe/stagehand/utils.ts +++ b/examples/integrations/stripe/stagehand/utils.ts @@ -44,13 +44,13 @@ export function validateZodSchema(schema: z.ZodTypeAny, data: unknown) { export async function drawObserveOverlay(page: Page, results: ObserveResult[]) { // Convert single xpath to array for consistent handling - const xpathList = results.map((result) => result.selector); + const xpathList = results.map(result => result.selector); // Filter out empty xpaths - const validXpaths = xpathList.filter((xpath) => xpath !== "xpath="); + const validXpaths = xpathList.filter(xpath => xpath !== "xpath="); - await page.evaluate((selectors) => { - selectors.forEach((selector) => { + await page.evaluate(selectors => { + selectors.forEach(selector => { let element; if (selector.startsWith("xpath=")) { const xpath = selector.substring(6); @@ -87,7 +87,7 @@ export async function clearOverlays(page: Page) { // remove existing stagehandObserve attributes await page.evaluate(() => { const elements = document.querySelectorAll('[stagehandObserve="true"]'); - elements.forEach((el) => { + elements.forEach(el => { const parent = el.parentNode; while (el.firstChild) { parent?.insertBefore(el.firstChild, el); diff --git a/examples/integrations/trigger/README.md b/examples/integrations/trigger/README.md index 0a199fc..b03c1c7 100644 --- a/examples/integrations/trigger/README.md +++ b/examples/integrations/trigger/README.md @@ -13,9 +13,11 @@ Trigger.dev is a background job framework that enables you to create, run, and m ## Task Examples ### 1. PDF to Image Conversion (`pdf-to-image.tsx`) + Converts PDF documents to PNG images using MuPDF and uploads them to Cloudflare R2 storage. **Features:** + - Downloads PDF from URL - Converts each page to PNG using `mutool` - Uploads images to R2 bucket @@ -25,31 +27,40 @@ Converts PDF documents to PNG images using MuPDF and uploads them to Cloudflare ### 2. Puppeteer Web Scraping (`puppeteer-*.tsx`) #### Basic Page Title Extraction + Simple example that launches Puppeteer, navigates to Google, and logs the page title. #### Scraping with Browserbase Proxy + Uses Browserbase's cloud browser infrastructure to scrape data through a proxy: + - Connects to Browserbase WebSocket endpoint - Scrapes GitHub star count from trigger.dev website - Handles errors gracefully #### Webpage to PDF Generation + Converts web pages to PDF documents: + - Navigates to target URL - Generates PDF from webpage - Uploads PDF to cloud storage ### 3. React PDF Generation (`react-pdf.tsx`) + Creates PDF documents using React components and @react-pdf/renderer: + - Accepts text payload - Renders PDF using React components - Uploads generated PDF to cloud storage - Returns PDF URL ### 4. Hacker News Summarization (`summarize-hn.tsx`) + Scheduled task that runs weekdays at 9 AM (London time): **Workflow:** + 1. **Scrapes Hacker News** - Gets top 3 articles 2. **Batch Processing** - Triggers child tasks for each article 3. **Content Extraction** - Scrapes full article content @@ -57,6 +68,7 @@ Scheduled task that runs weekdays at 9 AM (London time): 5. **Email Delivery** - Sends formatted email with summaries **Features:** + - Scheduled execution with cron syntax - Batch task processing with `batchTriggerAndWait` - Retry logic with exponential backoff @@ -64,7 +76,9 @@ Scheduled task that runs weekdays at 9 AM (London time): - Email templates using React Email ### 5. Task Hierarchy (`taskHierarchy.ts`) + Demonstrates complex task workflows with parent-child relationships: + - **Root Task** → **Child Task** → **Grandchild Task** → **Great-grandchild Task** - Shows both synchronous (`triggerAndWait`) and asynchronous (`trigger`) patterns - Batch processing capabilities @@ -73,8 +87,8 @@ Demonstrates complex task workflows with parent-child relationships: ## Configuration ### Trigger.dev Config (`trigger.config.ts`) -```typescript +```typescript export default defineConfig({ project: "proj_ljbidlufugyxuhjxzkyy", // your Trigger Project ID logLevel: "log", @@ -89,15 +103,13 @@ export default defineConfig({ }, }, build: { - extensions: [ - aptGet({ packages: ["mupdf-tools", "curl"] }), - puppeteer(), - ], + extensions: [aptGet({ packages: ["mupdf-tools", "curl"] }), puppeteer()], }, }); ``` **Key Features:** + - **System Dependencies**: Installs MuPDF tools and curl via `aptGet` extension - **Puppeteer Extension**: Automatically sets up Puppeteer with Chrome - **Retry Configuration**: Global retry settings with exponential backoff @@ -149,19 +161,22 @@ RESEND_API_KEY=your-resend-api-key ## Getting Started 1. **Install dependencies:** + ```bash npm install ``` 2. **Set up environment variables:** -Copy the environment variables above into a `.env.local` file and fill in your actual values. + Copy the environment variables above into a `.env.local` file and fill in your actual values. 3. **Start development server:** + ```bash npm run dev ``` 4. **Deploy to Trigger.dev:** + ```bash npx trigger.dev@latest deploy ``` @@ -169,6 +184,7 @@ npx trigger.dev@latest deploy ## Machine Presets Tasks can specify machine requirements: + ```typescript export const puppeteerBasicTask = task({ id: "puppeteer-log-title", @@ -184,6 +200,7 @@ Available presets provide different CPU/memory configurations for resource-inten ## Error Handling & Retries All tasks include comprehensive error handling: + - **Automatic retries** with exponential backoff - **Resource cleanup** (browser instances, temporary files) - **Detailed logging** for debugging @@ -192,6 +209,7 @@ All tasks include comprehensive error handling: ## Integration Services This example integrates with several external services: + - **Browserbase**: Cloud browser infrastructure for web scraping - **Cloudflare R2**: Object storage for files - **OpenAI**: AI-powered content summarization @@ -201,6 +219,7 @@ This example integrates with several external services: ## Use Cases Perfect for: + - **Document processing workflows** - **Web scraping and data extraction** - **Automated content generation** diff --git a/examples/integrations/trigger/src/app/api/route.ts b/examples/integrations/trigger/src/app/api/route.ts index 8ed940e..5714914 100644 --- a/examples/integrations/trigger/src/app/api/route.ts +++ b/examples/integrations/trigger/src/app/api/route.ts @@ -33,7 +33,7 @@ export async function GET() { // Clean up fs.unlinkSync(pdfPath); - images.forEach((img) => fs.unlinkSync(path.join(outputDir, img))); + images.forEach(img => fs.unlinkSync(path.join(outputDir, img))); fs.rmdirSync(outputDir); return NextResponse.json({ diff --git a/examples/integrations/trigger/src/trigger/summarize-hn.tsx b/examples/integrations/trigger/src/trigger/summarize-hn.tsx index 142af9f..a8f6cad 100644 --- a/examples/integrations/trigger/src/trigger/summarize-hn.tsx +++ b/examples/integrations/trigger/src/trigger/summarize-hn.tsx @@ -33,7 +33,7 @@ export const summarizeHackerNews = schedules.task({ const items = document.querySelectorAll(".athing"); return Array.from(items) .slice(0, 3) - .map((item) => { + .map(item => { const titleElement = item.querySelector(".titleline > a"); const link = titleElement?.getAttribute("href"); const title = titleElement?.textContent; @@ -48,14 +48,12 @@ export const summarizeHackerNews = schedules.task({ // Use batchTriggerAndWait to process articles const summaries = await scrapeAndSummarizeArticle .batchTriggerAndWait( - articles.map((article) => ({ + articles.map(article => ({ payload: { title: article.title!, link: article.link! }, idempotencyKey: article.link, })) ) - .then((batch) => - batch.runs.filter((run) => run.ok).map((run) => run.output) - ); + .then(batch => batch.runs.filter(run => run.ok).map(run => run.output)); // Send email using Resend await resend.emails.send({ @@ -89,7 +87,7 @@ export const scrapeAndSummarizeArticle = task({ // Prevent all assets from loading, images, stylesheets etc await page.setRequestInterception(true); - page.on("request", (request) => { + page.on("request", request => { if ( ["script", "stylesheet", "image", "media", "font"].includes( request.resourceType() diff --git a/examples/integrations/vercel/BrowseGPT/app/api/chat/route.ts b/examples/integrations/vercel/BrowseGPT/app/api/chat/route.ts index 31e5392..f0c0853 100644 --- a/examples/integrations/vercel/BrowseGPT/app/api/chat/route.ts +++ b/examples/integrations/vercel/BrowseGPT/app/api/chat/route.ts @@ -1,23 +1,26 @@ -import { openai } from '@ai-sdk/openai'; -import { streamText, convertToCoreMessages, tool, generateText } from 'ai'; -import { z } from 'zod'; -import { chromium } from 'playwright'; -import {anthropic} from '@ai-sdk/anthropic' -import { Readability } from '@mozilla/readability'; -import { JSDOM } from 'jsdom'; +import { openai } from "@ai-sdk/openai"; +import { streamText, convertToCoreMessages, tool, generateText } from "ai"; +import { z } from "zod"; +import { chromium } from "playwright"; +import { anthropic } from "@ai-sdk/anthropic"; +import { Readability } from "@mozilla/readability"; +import { JSDOM } from "jsdom"; -const bb_api_key = process.env.BROWSERBASE_API_KEY! -const bb_project_id = process.env.BROWSERBASE_PROJECT_ID! +const bb_api_key = process.env.BROWSERBASE_API_KEY!; +const bb_project_id = process.env.BROWSERBASE_PROJECT_ID!; // Helper functions (not exported) async function getDebugUrl(id: string) { - const response = await fetch(`https://www.browserbase.com/v1/sessions/${id}/debug`, { - method: "GET", - headers: { - "x-bb-api-key": bb_api_key, - "Content-Type": "application/json", - }, - }); + const response = await fetch( + `https://www.browserbase.com/v1/sessions/${id}/debug`, + { + method: "GET", + headers: { + "x-bb-api-key": bb_api_key, + "Content-Type": "application/json", + }, + } + ); const data = await response.json(); return data; } @@ -31,8 +34,8 @@ async function createSession() { }, body: JSON.stringify({ projectId: bb_project_id, - keepAlive: true - }), + keepAlive: true, + }), }); const data = await response.json(); return { id: data.id, debugUrl: data.debugUrl }; @@ -47,76 +50,96 @@ export async function POST(req: Request) { const result = await streamText({ experimental_toolCallStreaming: true, - model: openai('gpt-4-turbo'), + model: openai("gpt-4-turbo"), // model: openai('gpt-4o'), // model: anthropic('claude-3-5-sonnet-20240620'), messages: convertToCoreMessages(messages), tools: { createSession: tool({ - description: 'Create a new session', + description: "Create a new session", parameters: z.object({}), execute: async () => { const session = await createSession(); const debugUrl = await getDebugUrl(session.id); - return { sessionId: session.id, debugUrl: debugUrl.debuggerFullscreenUrl, toolName: 'Creating a new session'}; + return { + sessionId: session.id, + debugUrl: debugUrl.debuggerFullscreenUrl, + toolName: "Creating a new session", + }; }, }), askForConfirmation: tool({ - description: 'Ask the user for confirmation.', + description: "Ask the user for confirmation.", parameters: z.object({ - message: z.string().describe('The message to ask for confirmation.'), + message: z.string().describe("The message to ask for confirmation."), }), }), googleSearch: tool({ - description: 'Search Google for a query', + description: "Search Google for a query", parameters: z.object({ - toolName: z.string().describe('What the tool is doing'), - query: z.string().describe('The exact and complete search query as provided by the user. Do not modify this in any way.'), - sessionId: z.string().describe('The session ID to use for the search. If there is no session ID, create a new session with createSession Tool.'), - debuggerFullscreenUrl: z.string().describe('The fullscreen debug URL to use for the search. If there is no debug URL, create a new session with createSession Tool.') + toolName: z.string().describe("What the tool is doing"), + query: z + .string() + .describe( + "The exact and complete search query as provided by the user. Do not modify this in any way." + ), + sessionId: z + .string() + .describe( + "The session ID to use for the search. If there is no session ID, create a new session with createSession Tool." + ), + debuggerFullscreenUrl: z + .string() + .describe( + "The fullscreen debug URL to use for the search. If there is no debug URL, create a new session with createSession Tool." + ), }), execute: async ({ query, sessionId }) => { try { - const browser = await chromium.connectOverCDP( `wss://connect.browserbase.com?apiKey=${bb_api_key}&sessionId=${sessionId}` ); const defaultContext = browser.contexts()[0]; const page = defaultContext.pages()[0]; - - await page.goto(`https://www.google.com/search?q=${encodeURIComponent(query)}`); + + await page.goto( + `https://www.google.com/search?q=${encodeURIComponent(query)}` + ); await page.waitForTimeout(500); - await page.keyboard.press('Enter'); - await page.waitForLoadState('load', { timeout: 10000 }); - - await page.waitForSelector('.g'); + await page.keyboard.press("Enter"); + await page.waitForLoadState("load", { timeout: 10000 }); + + await page.waitForSelector(".g"); const results = await page.evaluate(() => { - const items = document.querySelectorAll('.g'); + const items = document.querySelectorAll(".g"); return Array.from(items).map(item => { - const title = item.querySelector('h3')?.textContent || ''; - const description = item.querySelector('.VwiC3b')?.textContent || ''; + const title = item.querySelector("h3")?.textContent || ""; + const description = + item.querySelector(".VwiC3b")?.textContent || ""; return { title, description }; }); }); - - const text = results.map(item => `${item.title}\n${item.description}`).join('\n\n'); + + const text = results + .map(item => `${item.title}\n${item.description}`) + .join("\n\n"); const response = await generateText({ // model: openai('gpt-4-turbo'), - model: anthropic('claude-3-5-sonnet-20240620'), + model: anthropic("claude-3-5-sonnet-20240620"), prompt: `Evaluate the following web page content: ${text}`, }); return { - toolName: 'Searching Google', + toolName: "Searching Google", content: response.text, dataCollected: true, }; } catch (error) { - console.error('Error in googleSearch:', error); + console.error("Error in googleSearch:", error); return { - toolName: 'Searching Google', + toolName: "Searching Google", content: `Error performing Google search: ${error}`, dataCollected: false, }; @@ -124,45 +147,52 @@ export async function POST(req: Request) { }, }), getPageContent: tool({ - description: 'Get the content of a page using Playwright', + description: "Get the content of a page using Playwright", parameters: z.object({ - toolName: z.string().describe('What the tool is doing'), - url: z.string().describe('The url to get the content of'), - sessionId: z.string().describe('The session ID to use for the search. If there is no session ID, create a new session with createSession Tool.'), - debuggerFullscreenUrl: z.string().describe('The fullscreen debug URL to use for the search. If there is no debug URL, create a new session with createSession Tool.') + toolName: z.string().describe("What the tool is doing"), + url: z.string().describe("The url to get the content of"), + sessionId: z + .string() + .describe( + "The session ID to use for the search. If there is no session ID, create a new session with createSession Tool." + ), + debuggerFullscreenUrl: z + .string() + .describe( + "The fullscreen debug URL to use for the search. If there is no debug URL, create a new session with createSession Tool." + ), }), execute: async ({ url, sessionId }) => { try { - const browser = await chromium.connectOverCDP( `wss://connect.browserbase.com?apiKey=${process.env.BROWSERBASE_API_KEY}&sessionId=${sessionId}` ); const defaultContext = browser.contexts()[0]; const page = defaultContext.pages()[0]; - + await page.goto(url); - + const content = await page.content(); const dom = new JSDOM(content); const reader = new Readability(dom.window.document); const article = reader.parse(); - const text = `${article?.title || ''}\n${article?.textContent || ''}`; + const text = `${article?.title || ""}\n${article?.textContent || ""}`; const response = await generateText({ // model: openai('gpt-4-turbo'), - model: anthropic('claude-3-5-sonnet-20240620'), + model: anthropic("claude-3-5-sonnet-20240620"), prompt: `Evaluate the following web page content: ${text}`, }); return { - toolName: 'Getting page content', + toolName: "Getting page content", content: response.text, }; } catch (error) { - console.error('Error in getPageContent:', error); + console.error("Error in getPageContent:", error); return { - toolName: 'Getting page content', + toolName: "Getting page content", content: `Error fetching page content: ${error}`, }; } @@ -172,4 +202,4 @@ export async function POST(req: Request) { }); return result.toDataStreamResponse(); -} \ No newline at end of file +} diff --git a/examples/integrations/vercel/BrowseGPT/app/layout.tsx b/examples/integrations/vercel/BrowseGPT/app/layout.tsx index 96fee5a..6f62c0f 100644 --- a/examples/integrations/vercel/BrowseGPT/app/layout.tsx +++ b/examples/integrations/vercel/BrowseGPT/app/layout.tsx @@ -1,12 +1,13 @@ import type { Metadata } from "next"; // import localFont from "next/font/local"; -import { GeistSans } from 'geist/font/sans'; +import { GeistSans } from "geist/font/sans"; import "./globals.css"; export const metadata: Metadata = { title: "BrowseGPT", - description: "BrowseGPT is a chat interface that allows you to search the web and get answers to your questions.", + description: + "BrowseGPT is a chat interface that allows you to search the web and get answers to your questions.", }; export default function RootLayout({ @@ -16,10 +17,7 @@ export default function RootLayout({ }>) { return ( - - - {children} - + {children} ); } diff --git a/examples/integrations/vercel/BrowseGPT/app/page.tsx b/examples/integrations/vercel/BrowseGPT/app/page.tsx index 6126770..9e17038 100644 --- a/examples/integrations/vercel/BrowseGPT/app/page.tsx +++ b/examples/integrations/vercel/BrowseGPT/app/page.tsx @@ -1,33 +1,34 @@ -'use client'; - -import { useChat } from 'ai/react'; -import { useState, useEffect } from 'react'; -import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; -import Markdown from 'react-markdown'; -import { MarkdownWrapper } from '@/components/ui/markdown'; -import remarkGfm from 'remark-gfm'; +"use client"; + +import { useChat } from "ai/react"; +import { useState, useEffect } from "react"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; +import Markdown from "react-markdown"; +import { MarkdownWrapper } from "@/components/ui/markdown"; +import remarkGfm from "remark-gfm"; import BlurFade from "@/components/ui/blur-fade"; // import Spinner from "@/components/spinner"; import VercelLogo from "@/components/vercel"; -import BrowserbaseLogo from "@/components/browserbase" -import FlickeringGrid from '@/components/ui/flickering-grid'; -import FlickeringLoad from '@/components/ui/flickering-load'; -import { Prompts } from '@/components/prompts'; +import BrowserbaseLogo from "@/components/browserbase"; +import FlickeringGrid from "@/components/ui/flickering-grid"; +import FlickeringLoad from "@/components/ui/flickering-load"; +import { Prompts } from "@/components/prompts"; export default function Chat() { - const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({ - maxSteps: 5, - }); + const { messages, input, handleInputChange, handleSubmit, isLoading } = + useChat({ + maxSteps: 5, + }); const [showAlert, setShowAlert] = useState(false); - const [statusMessage, setStatusMessage] = useState(''); + const [statusMessage, setStatusMessage] = useState(""); const [sessionId, setSessionId] = useState(null); const [hasInteracted, setHasInteracted] = useState(false); const isGenerating = isLoading && (!messages.length || - messages[messages.length - 1].role !== 'assistant' || + messages[messages.length - 1].role !== "assistant" || !messages[messages.length - 1].content); useEffect(() => { @@ -38,19 +39,24 @@ export default function Chat() { // Check if any tool invocation has dataCollected = true const dataCollected = lastMessage?.toolInvocations?.some( - invocation => 'result' in invocation && - typeof invocation.result === 'object' && - invocation.result !== null && - 'dataCollected' in invocation.result && - invocation.result.dataCollected === true + invocation => + "result" in invocation && + typeof invocation.result === "object" && + invocation.result !== null && + "dataCollected" in invocation.result && + invocation.result.dataCollected === true ); if (dataCollected && !lastMessage.content) { // The AI has collected data and is generating a response - setStatusMessage('The AI has collected data and is generating a response. Please wait.'); + setStatusMessage( + "The AI has collected data and is generating a response. Please wait." + ); } else { // The AI is currently processing the request - setStatusMessage('The AI is currently processing your request. Please wait.'); + setStatusMessage( + "The AI is currently processing your request. Please wait." + ); } setSessionId(null); @@ -63,7 +69,7 @@ export default function Chat() { const lastMessage = messages[messages.length - 1]; if (lastMessage?.toolInvocations) { for (const invocation of lastMessage.toolInvocations) { - if ('result' in invocation && invocation.result?.sessionId) { + if ("result" in invocation && invocation.result?.sessionId) { setSessionId(invocation.result.sessionId); break; } @@ -73,17 +79,21 @@ export default function Chat() { const handleSubmitWrapper = (e: React.FormEvent) => { e.preventDefault(); - setHasInteracted(true); + setHasInteracted(true); handleSubmit(e, { data: { message: input } }); }; const handlePromptClick = (text: string) => { setHasInteracted(true); // Set the input value - handleInputChange({ target: { value: text } } as React.ChangeEvent); + handleInputChange({ + target: { value: text }, + } as React.ChangeEvent); // Submit the form after a short delay setTimeout(() => { - const submitButton = document.querySelector('button[type="submit"]') as HTMLButtonElement; + const submitButton = document.querySelector( + 'button[type="submit"]' + ) as HTMLButtonElement; if (submitButton) { submitButton.click(); } @@ -93,18 +103,34 @@ export default function Chat() { return (
-
+
{/* Header */}
- Made by AP + + Made by AP +

- + x - +

@@ -114,20 +140,26 @@ export default function Chat() { {/* Chat content */}
-
{/* Added px-4 */} +
+ {" "} + {/* Added px-4 */} {!hasInteracted && messages.length === 0 ? (
-

Welcome

- -

What web task can I conquer for you today?

+

+ Welcome +

+ +

+ What web task can I conquer for you today? +

) : ( messages.map((m, index) => (
- {m.role === 'user' ? ( + {m.role === "user" ? ( <> User:

{m.content}

@@ -137,8 +169,8 @@ export default function Chat() { {m.toolInvocations?.map((invocation, index) => { - let content = ''; - if ('result' in invocation) { + let content = ""; + if ("result" in invocation) { if (invocation.result?.sessionId) { content = `Session ID: ${invocation.result.sessionId}`; } else if (invocation.result?.content) { @@ -160,7 +192,9 @@ export default function Chat() { } return content ? (
-
{content}
+
+                                  {content}
+                                
) : null; })} @@ -170,19 +204,28 @@ export default function Chat() { ) : ( <> - - - + + + -AI:
- {m.content} + + {m.content} +
@@ -190,7 +233,6 @@ export default function Chat() {
)) )} - {showAlert && !sessionId && ( @@ -198,19 +240,19 @@ export default function Chat() {
{messages[messages.length - 1].toolInvocations - ?.map((invocation) => { - if ('result' in invocation) { + ?.map(invocation => { + if ("result" in invocation) { return invocation.result?.toolName; } return invocation.args?.toolName; }) .filter(Boolean) - .join(', ')} + .join(", ")} {statusMessage}
{/*
*/} - + {/*
*/}
@@ -244,4 +286,4 @@ export default function Chat() {
); -} \ No newline at end of file +} diff --git a/examples/integrations/vercel/BrowseGPT/components.json b/examples/integrations/vercel/BrowseGPT/components.json index bcec1f9..d66adba 100644 --- a/examples/integrations/vercel/BrowseGPT/components.json +++ b/examples/integrations/vercel/BrowseGPT/components.json @@ -17,4 +17,4 @@ "lib": "@/lib", "hooks": "@/hooks" } -} \ No newline at end of file +} diff --git a/examples/integrations/vercel/BrowseGPT/components/browserbase.tsx b/examples/integrations/vercel/BrowseGPT/components/browserbase.tsx index 3306d6c..9042cde 100644 --- a/examples/integrations/vercel/BrowseGPT/components/browserbase.tsx +++ b/examples/integrations/vercel/BrowseGPT/components/browserbase.tsx @@ -5,25 +5,44 @@ export default function BrowserbaseLogo() { height="22" role="img" viewBox="0 0 663 647" - style={{ width: 'auto', overflow: 'visible', marginTop: '-2px' }} + style={{ width: "auto", overflow: "visible", marginTop: "-2px" }} > - - - - - + + + + + - - - + + + - + ); } - diff --git a/examples/integrations/vercel/BrowseGPT/components/prompts.tsx b/examples/integrations/vercel/BrowseGPT/components/prompts.tsx index 7397cb2..94e9643 100644 --- a/examples/integrations/vercel/BrowseGPT/components/prompts.tsx +++ b/examples/integrations/vercel/BrowseGPT/components/prompts.tsx @@ -3,7 +3,17 @@ import BlurFade from "@/components/ui/blur-fade"; import { Chrome, Newspaper, Palmtree } from "lucide-react"; // Update PromptCard to include onClick prop -const PromptCard = ({ icon, text, index, onClick }: { icon: React.ReactNode; text: string; index: number; onClick: () => void }) => ( +const PromptCard = ({ + icon, + text, + index, + onClick, +}: { + icon: React.ReactNode; + text: string; + index: number; + onClick: () => void; +}) => ( -
{icon}
-

{text}

+
+ {icon} +
+

+ {text} +

); // Update Prompts to accept onPromptClick prop -export function Prompts({ onPromptClick }: { onPromptClick: (text: string) => void }) { +export function Prompts({ + onPromptClick, +}: { + onPromptClick: (text: string) => void; +}) { return (
@@ -38,7 +56,9 @@ export function Prompts({ onPromptClick }: { onPromptClick: (text: string) => vo text="What's on Y Combinator" index={1} onClick={() => { - onPromptClick("What's the top recent news about on https://news.ycombinator.com/") + onPromptClick( + "What's the top recent news about on https://news.ycombinator.com/" + ); }} /> vo text="Tell me fun activities in SF" index={2} onClick={() => { - onPromptClick("Tell me fun activities in SF") + onPromptClick("Tell me fun activities in SF"); }} />
diff --git a/examples/integrations/vercel/BrowseGPT/components/spinner.tsx b/examples/integrations/vercel/BrowseGPT/components/spinner.tsx index e269dbe..5ba9d3f 100644 --- a/examples/integrations/vercel/BrowseGPT/components/spinner.tsx +++ b/examples/integrations/vercel/BrowseGPT/components/spinner.tsx @@ -1,10 +1,22 @@ -import React from 'react'; +import React from "react"; const Spinner: React.FC = () => ( -
{children}
diff --git a/examples/integrations/vercel/BrowseGPT/components/ui/markdown.tsx b/examples/integrations/vercel/BrowseGPT/components/ui/markdown.tsx index 617dfd7..c392a42 100644 --- a/examples/integrations/vercel/BrowseGPT/components/ui/markdown.tsx +++ b/examples/integrations/vercel/BrowseGPT/components/ui/markdown.tsx @@ -1,10 +1,13 @@ -import React from 'react'; +import React from "react"; -export const MarkdownWrapper: React.FC = ({ children }) => ( +export const MarkdownWrapper: React.FC = ({ + children, +}) => (
{children} diff --git a/examples/integrations/vercel/BrowseGPT/components/vercel.tsx b/examples/integrations/vercel/BrowseGPT/components/vercel.tsx index 27d92cc..4be8a73 100644 --- a/examples/integrations/vercel/BrowseGPT/components/vercel.tsx +++ b/examples/integrations/vercel/BrowseGPT/components/vercel.tsx @@ -5,9 +5,12 @@ export default function VercelLogo() { height="22" role="img" viewBox="0 0 74 64" - style={{ width: 'auto', overflow: 'visible', marginTop: '-2px' }} + style={{ width: "auto", overflow: "visible", marginTop: "-2px" }} > - + ); -} \ No newline at end of file +} diff --git a/examples/integrations/vercel/BrowseGPT/lib/utils.ts b/examples/integrations/vercel/BrowseGPT/lib/utils.ts index bd0c391..a5ef193 100644 --- a/examples/integrations/vercel/BrowseGPT/lib/utils.ts +++ b/examples/integrations/vercel/BrowseGPT/lib/utils.ts @@ -1,6 +1,6 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); } diff --git a/examples/integrations/vercel/BrowseGPT/tailwind.config.ts b/examples/integrations/vercel/BrowseGPT/tailwind.config.ts index ebc9f38..1c63261 100644 --- a/examples/integrations/vercel/BrowseGPT/tailwind.config.ts +++ b/examples/integrations/vercel/BrowseGPT/tailwind.config.ts @@ -1,62 +1,62 @@ import type { Config } from "tailwindcss"; const config: Config = { - darkMode: ["class"], - content: [ + darkMode: ["class"], + content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - extend: { - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - } - } + extend: { + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + "1": "hsl(var(--chart-1))", + "2": "hsl(var(--chart-2))", + "3": "hsl(var(--chart-3))", + "4": "hsl(var(--chart-4))", + "5": "hsl(var(--chart-5))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, }, plugins: [require("tailwindcss-animate")], }; diff --git a/examples/integrations/vercel/vercel-puppeteer/README.md b/examples/integrations/vercel/vercel-puppeteer/README.md index 2072823..001560b 100644 --- a/examples/integrations/vercel/vercel-puppeteer/README.md +++ b/examples/integrations/vercel/vercel-puppeteer/README.md @@ -34,4 +34,5 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + # vercel-puppeteer diff --git a/examples/integrations/vercel/vercel-puppeteer/app/api/form/route.ts b/examples/integrations/vercel/vercel-puppeteer/app/api/form/route.ts index 75e8eb7..0322bbe 100644 --- a/examples/integrations/vercel/vercel-puppeteer/app/api/form/route.ts +++ b/examples/integrations/vercel/vercel-puppeteer/app/api/form/route.ts @@ -6,7 +6,7 @@ import { Stagehand, ObserveResult, LogLine } from "@browserbasehq/stagehand"; export async function GET() { try { const url = "https://file.1040.com/estimate/"; - + if (!url) { return NextResponse.json({ error: "URL is required" }, { status: 400 }); } @@ -34,7 +34,7 @@ export async function GET() { await stagehand.init(); // Block manifest worker to prevent PWA installation popup if needed - await stagehand.page.route("**/manifest.json", (route) => route.abort()); + await stagehand.page.route("**/manifest.json", route => route.abort()); // Go to the provided URL and wait for it to load await stagehand.page.goto(url, { @@ -67,7 +67,7 @@ export async function GET() { }; for (const [key, terms] of Object.entries(keywords)) { - if (terms.some((term) => description.toLowerCase().includes(term))) { + if (terms.some(term => description.toLowerCase().includes(term))) { return key; } } @@ -114,7 +114,7 @@ export async function GET() { // Return the url and the fields that were filled return NextResponse.json({ url: url, - fields: updatedFields.map((field) => ({ + fields: updatedFields.map(field => ({ name: field.description, value: field.arguments?.[0] || null, })), diff --git a/examples/integrations/vercel/vercel-puppeteer/app/components/ExistingUrl.tsx b/examples/integrations/vercel/vercel-puppeteer/app/components/ExistingUrl.tsx index e5fdf7f..9860747 100644 --- a/examples/integrations/vercel/vercel-puppeteer/app/components/ExistingUrl.tsx +++ b/examples/integrations/vercel/vercel-puppeteer/app/components/ExistingUrl.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import Image from 'next/image'; +import React from "react"; +import Image from "next/image"; interface ExistingUrlProps { onSelect: (url: string) => void; @@ -8,39 +8,93 @@ interface ExistingUrlProps { // Move example URLs outside component to prevent recreation on each render const EXAMPLE_URLS = [ { - url: 'https://docs.browserbase.com', - title: 'Browserbase', + url: "https://docs.browserbase.com", + title: "Browserbase", icon: ( - - - + + + ), }, { - url: 'https://docs.stagehand.dev', - title: 'Stagehand', - icon: , + url: "https://docs.stagehand.dev", + title: "Stagehand", + icon: ( + + ), }, { - url: 'https://nextjs.org/docs', - title: 'Next.js', + url: "https://nextjs.org/docs", + title: "Next.js", icon: ( - - + + - - + + - + - + @@ -52,8 +106,10 @@ const EXAMPLE_URLS = [ const ExistingUrl: React.FC = ({ onSelect }) => (
-

Try a Document URL:

- +

+ Try a Document URL: +

+
{EXAMPLE_URLS.map(({ url, title, icon }) => (
- + diff --git a/package.json b/package.json index 8684bed..449d95a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "format:check": "prettier --check .", "typecheck": "pnpm -r typecheck", "clean": "pnpm -r clean", - "publish:packages": "pnpm -r publish" + "publish:packages": "pnpm -r publish", + "prepare": "husky", + "pre-commit": "lint-staged" }, "keywords": [ "browserbase", @@ -39,9 +41,20 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "globals": "^15.11.0", + "husky": "^9.0.11", + "lint-staged": "^15.2.10", "prettier": "^3.3.3", "typescript": "^5.6.3", "typescript-eslint": "^8.12.2", "vitest": "^2.1.4" + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,md,yml,yaml}": [ + "prettier --write" + ] } } diff --git a/tsconfig.json b/tsconfig.json index 101262c..876529e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,13 +30,7 @@ }, "include": [ "examples/integrations/src/**/*", - "examples/integrations/tests/**/*", + "examples/integrations/tests/**/*" ], - "exclude": [ - "node_modules", - "dist", - "build", - "out", - "coverage" - ] -} \ No newline at end of file + "exclude": ["node_modules", "dist", "build", "out", "coverage"] +}