From 8ba5e2b0c86d23466e3b8d1793e7777258c931d2 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Wed, 9 Sep 2026 16:35:06 +0000 Subject: [PATCH 1/4] Consolidate reviewed Stagehand examples and link external skills --- oxlint.config.ts | 3 + packages/examples/.gitignore | 13 + packages/examples/README.md | 95 +++ .../demos/caching-with-variables/.env.example | 9 + .../demos/caching-with-variables/.gitignore | 24 + .../demos/caching-with-variables/README.md | 227 +++++++ .../demos/caching-with-variables/package.json | 24 + .../src/01-act-cache-with-variables.ts | 268 ++++++++ .../src/02-agent-cache-test.ts | 280 +++++++++ .../src/act-cache-demo.ts | 51 ++ .../caching-with-variables/tsconfig.json | 16 + .../manifests/company-news-finder.json | 11 + .../demos/company-news-function/.env.example | 10 + .../demos/company-news-function/.gitignore | 24 + .../demos/company-news-function/README.md | 371 +++++++++++ .../demos/company-news-function/index.ts | 116 ++++ .../demos/company-news-function/package.json | 19 + .../demos/company-news-function/tsconfig.json | 19 + .../configurable-browser-trial/.env.example | 10 + .../configurable-browser-trial/.gitignore | 5 + .../configurable-browser-trial/README.md | 182 ++++++ .../configurable-browser-trial/bin/bbpoc.mjs | 28 + .../examples/trial.example.yaml | 47 ++ .../configurable-browser-trial/package.json | 34 ++ .../src/classify.ts | 145 +++++ .../configurable-browser-trial/src/cli.ts | 201 ++++++ .../configurable-browser-trial/src/config.ts | 96 +++ .../configurable-browser-trial/src/pool.ts | 24 + .../src/report/html.ts | 156 +++++ .../src/report/markdown.ts | 75 +++ .../configurable-browser-trial/src/runner.ts | 169 +++++ .../src/scorecard.ts | 85 +++ .../configurable-browser-trial/src/types.ts | 135 ++++ .../configurable-browser-trial/tsconfig.json | 15 + .../hacker-news-intelligence/.env.example | 12 + .../demos/hacker-news-intelligence/.gitignore | 35 ++ .../demos/hacker-news-intelligence/README.md | 309 ++++++++++ .../hacker-news-intelligence/package.json | 34 ++ .../hacker-news-intelligence/src/config.ts | 39 ++ .../hacker-news-intelligence/src/extractor.ts | 263 ++++++++ .../hacker-news-intelligence/src/logger.ts | 83 +++ .../hacker-news-intelligence/src/main.ts | 222 +++++++ .../hacker-news-intelligence/src/reporter.ts | 245 ++++++++ .../hacker-news-intelligence/src/test.ts | 114 ++++ .../hacker-news-intelligence/src/types.ts | 70 +++ .../hacker-news-intelligence/tsconfig.json | 19 + packages/examples/demos/qa-agent/README.md | 228 +++++++ .../demos/qa-agent/buggy-app/next-env.d.ts | 5 + .../demos/qa-agent/buggy-app/next.config.js | 3 + .../demos/qa-agent/buggy-app/package.json | 24 + .../qa-agent/buggy-app/postcss.config.js | 6 + .../qa-agent/buggy-app/src/app/about/page.tsx | 40 ++ .../qa-agent/buggy-app/src/app/cart/page.tsx | 71 +++ .../buggy-app/src/app/checkout/page.tsx | 20 + .../qa-agent/buggy-app/src/app/globals.css | 32 + .../qa-agent/buggy-app/src/app/layout.tsx | 22 + .../demos/qa-agent/buggy-app/src/app/page.tsx | 24 + .../buggy-app/src/app/product/[id]/page.tsx | 68 +++ .../buggy-app/src/components/CartSummary.tsx | 35 ++ .../buggy-app/src/components/CheckoutForm.tsx | 172 ++++++ .../buggy-app/src/components/Navbar.tsx | 35 ++ .../buggy-app/src/components/ProductCard.tsx | 41 ++ .../qa-agent/buggy-app/src/lib/cart-store.ts | 71 +++ .../qa-agent/buggy-app/src/lib/products.ts | 72 +++ .../qa-agent/buggy-app/tailwind.config.js | 6 + .../demos/qa-agent/buggy-app/tsconfig.json | 20 + .../demos/qa-agent/qa-agent/.env.example | 5 + .../demos/qa-agent/qa-agent/package.json | 23 + .../qa-agent/qa-agent/src/approach-a/run.ts | 92 +++ .../qa-agent/qa-agent/src/approach-a/tools.ts | 486 +++++++++++++++ .../qa-agent/qa-agent/src/approach-b/run.ts | 202 ++++++ .../demos/qa-agent/qa-agent/src/index.ts | 14 + .../qa-agent/src/shared/stagehand-init.ts | 27 + .../qa-agent/qa-agent/src/shared/types.ts | 29 + .../demos/qa-agent/qa-agent/tsconfig.json | 13 + .../examples/demos/v4-demo-kit/.env.example | 15 + .../examples/demos/v4-demo-kit/.gitignore | 3 + packages/examples/demos/v4-demo-kit/README.md | 143 +++++ .../examples/demos/v4-demo-kit/package.json | 25 + .../scripts/research-hacker-news.mjs | 23 + .../demos/v4-demo-kit/src/agent-harness.mjs | 90 +++ .../examples/demos/v4-demo-kit/src/cli.mjs | 124 ++++ .../demos/v4-demo-kit/src/http-server.mjs | 42 ++ .../demos/v4-demo-kit/src/session.mjs | 44 ++ .../examples/demos/v4-demo-kit/src/tools.mjs | 134 ++++ .../demos/v4-demo-kit/test/tools.test.mjs | 30 + .../demos/web-performance/.env.example | 7 + .../examples/demos/web-performance/README.md | 169 +++++ .../demos/web-performance/package.json | 22 + .../web-performance/src/perf-vitals-agent.ts | 537 ++++++++++++++++ .../demos/web-performance/tsconfig.json | 15 + packages/examples/integrations/LICENSE | 7 + .../examples/integrations/agentkit/README.md | 100 +++ .../integrations/agentkit/package.json | 20 + .../integrations/agentkit/src/index.ts | 141 +++++ .../agentkit/src/stagehand-tools.ts | 84 +++ .../integrations/agentkit/src/utils.ts | 85 +++ .../integrations/agentkit/tsconfig.json | 16 + .../examples/integrations/box/.env.example | 16 + packages/examples/integrations/box/README.md | 105 ++++ .../examples/integrations/box/package.json | 19 + packages/examples/integrations/box/src/box.ts | 221 +++++++ .../integrations/box/src/browserbase.ts | 162 +++++ .../integrations/box/src/compliance.ts | 66 ++ .../examples/integrations/box/src/index.ts | 79 +++ .../examples/integrations/box/tsconfig.json | 16 + .../examples/integrations/convex/.gitignore | 2 + .../examples/integrations/convex/README.md | 210 +++++++ .../convex/convex/convex.config.ts | 7 + .../integrations/convex/convex/example.ts | 287 +++++++++ .../integrations/convex/convex/schema.ts | 14 + .../examples/integrations/convex/package.json | 19 + .../integrations/convex/tsconfig.json | 18 + .../examples/integrations/crewai/.env.example | 3 + .../examples/integrations/crewai/.gitignore | 1 + .../examples/integrations/crewai/README.md | 21 + packages/examples/integrations/crewai/main.py | 56 ++ .../integrations/crewai/requirements.txt | 226 +++++++ .../integrations/deepagents/README.md | 100 +++ .../integrations/deepagents/browser_tools.py | 267 ++++++++ .../examples/integrations/deepagents/main.py | 228 +++++++ .../integrations/deepagents/requirements.txt | 6 + .../examples/integrations/langchain/README.md | 35 ++ .../integrations/langchain/package.json | 19 + .../integrations/langchain/src/index.js | 32 + .../examples/integrations/mastra/.gitignore | 9 + .../examples/integrations/mastra/README.md | 95 +++ .../examples/integrations/mastra/package.json | 28 + .../mastra/src/mastra/agents/index.ts | 32 + .../integrations/mastra/src/mastra/index.ts | 11 + .../mastra/src/mastra/tools/index.ts | 338 ++++++++++ .../integrations/mastra/tsconfig.json | 15 + .../examples/integrations/mongodb/README.md | 214 +++++++ .../integrations/mongodb/python/.gitignore | 2 + .../integrations/mongodb/python/README.md | 243 ++++++++ .../integrations/mongodb/python/env.example | 10 + .../integrations/mongodb/python/main.py | 577 ++++++++++++++++++ .../mongodb/python/requirements.txt | 6 + .../mongodb/typescript/.env.example | 12 + .../mongodb/typescript/.gitignore | 7 + .../integrations/mongodb/typescript/LICENSE | 7 + .../integrations/mongodb/typescript/README.md | 103 ++++ .../integrations/mongodb/typescript/index.ts | 539 ++++++++++++++++ .../mongodb/typescript/package.json | 24 + .../mongodb/typescript/tsconfig.json | 17 + .../integrations/mongodb/typescript/utils.ts | 137 +++++ .../integrations/temporal/.env.example | 18 + .../integrations/temporal/.eslintignore | 3 + .../integrations/temporal/.eslintrc.js | 50 ++ .../examples/integrations/temporal/.gitignore | 9 + .../examples/integrations/temporal/.nvmrc | 1 + .../integrations/temporal/.prettierrc | 1 + .../examples/integrations/temporal/README.md | 117 ++++ .../integrations/temporal/jest.config.js | 7 + .../integrations/temporal/package.json | 49 ++ .../integrations/temporal/src/demo.ts | 29 + .../temporal/src/research-activities.ts | 324 ++++++++++ .../temporal/src/research-worker.ts | 24 + .../integrations/temporal/src/workflows.ts | 130 ++++ .../integrations/temporal/tsconfig.json | 12 + packages/examples/oxlint.config.ts | 7 + .../playbook/1password-extension/.env.example | 5 + .../playbook/1password-extension/README.md | 5 + .../node/createExtension.ts | 26 + .../1password-extension/node/stagehand.ts | 35 ++ .../playbook/1password-extension/package.json | 19 + .../python/create_extension.py | 30 + .../python/run_stagehand.py | 41 ++ .../playbook/alaska-flights/.env.example | 6 + .../playbook/alaska-flights/README.md | 5 + .../playbook/alaska-flights/package.json | 20 + .../alaska-flights/searchAlaskaFlights.ts | 86 +++ .../playbook/docs-search/.env.example | 4 + .../examples/playbook/docs-search/README.md | 5 + .../playbook/docs-search/package.json | 20 + .../playbook/docs-search/searchDocs.ts | 85 +++ .../playbook/hacker-news/.env.example | 4 + .../examples/playbook/hacker-news/README.md | 5 + .../playbook/hacker-news/package.json | 20 + .../examples/playbook/hacker-news/pullNews.ts | 59 ++ .../playbook/southwest-flights/.env.example | 4 + .../playbook/southwest-flights/README.md | 5 + .../playbook/southwest-flights/package.json | 20 + .../searchSouthwestFlights.ts | 73 +++ .../.env.example | 2 + .../amazon-global-price-comparison/README.md | 77 +++ .../amazon-global-price-comparison/main.py | 159 +++++ .../pyproject.toml | 25 + .../amazon-product-scraping/.env.example | 3 + .../python/amazon-product-scraping/README.md | 67 ++ .../python/amazon-product-scraping/main.py | 106 ++++ .../amazon-product-scraping/pyproject.toml | 30 + .../python/basic-caching/.env.example | 1 + .../templates/python/basic-caching/README.md | 128 ++++ .../templates/python/basic-caching/main.py | 74 +++ .../python/basic-caching/pyproject.toml | 8 + .../python/basic-caching/requirements.txt | 3 + .../python/basic-recaptcha/.env.example | 1 + .../python/basic-recaptcha/README.md | 108 ++++ .../templates/python/basic-recaptcha/main.py | 63 ++ .../python/basic-recaptcha/pyproject.toml | 8 + .../python/browserbase-reducto/README.md | 84 +++ .../python/browserbase-reducto/main.py | 202 ++++++ .../python/browserbase-reducto/pyproject.toml | 25 + .../company-value-prop-generator/.env.example | 1 + .../company-value-prop-generator/README.md | 60 ++ .../company-value-prop-generator/main.py | 82 +++ .../pyproject.toml | 8 + .../templates/python/context/.env.example | 3 + .../templates/python/context/README.md | 63 ++ .../examples/templates/python/context/main.py | 110 ++++ .../templates/python/context/pyproject.toml | 13 + .../python/council-events/.env.example | 1 + .../templates/python/council-events/README.md | 65 ++ .../templates/python/council-events/main.py | 83 +++ .../python/council-events/pyproject.toml | 8 + .../.env.example | 1 + .../download-financial-statements/README.md | 64 ++ .../download-financial-statements/main.py | 117 ++++ .../pyproject.toml | 8 + .../python/extend-browserbase/.env.example | 6 + .../python/extend-browserbase/README.md | 80 +++ .../python/extend-browserbase/main.py | 548 +++++++++++++++++ .../python/extend-browserbase/pyproject.toml | 30 + .../python/form-filling/.env.example | 1 + .../templates/python/form-filling/README.md | 62 ++ .../templates/python/form-filling/main.py | 82 +++ .../python/form-filling/pyproject.toml | 8 + .../templates/python/gift-finder/.env.example | 5 + .../templates/python/gift-finder/README.md | 69 +++ .../templates/python/gift-finder/main.py | 186 ++++++ .../python/gift-finder/pyproject.toml | 8 + .../python/google-trends/.env.example | 2 + .../templates/python/google-trends/README.md | 62 ++ .../templates/python/google-trends/main.py | 84 +++ .../python/google-trends/pyproject.toml | 25 + .../python/image-url-download/.env.example | 5 + .../python/image-url-download/.gitignore | 2 + .../python/image-url-download/README.md | 76 +++ .../python/image-url-download/main.py | 185 ++++++ .../python/image-url-download/pyproject.toml | 25 + .../python/job-application/.env.example | 4 + .../python/job-application/README.md | 19 + .../templates/python/job-application/main.py | 155 +++++ .../python/job-application/pyproject.toml | 8 + .../manual-mfa-with-contexts/.env.example | 3 + .../python/manual-mfa-with-contexts/README.md | 69 +++ .../python/manual-mfa-with-contexts/main.py | 132 ++++ .../manual-mfa-with-contexts/pyproject.toml | 13 + .../manual-mfa-with-contexts/requirements.txt | 5 + .../python/mfa-handling/.env.example | 1 + .../templates/python/mfa-handling/README.md | 78 +++ .../templates/python/mfa-handling/main.py | 118 ++++ .../python/mfa-handling/pyproject.toml | 8 + .../python/mfa-handling/requirements.txt | 3 + .../templates/python/pickleball/.env.example | 9 + .../templates/python/pickleball/README.md | 85 +++ .../templates/python/pickleball/main.py | 175 ++++++ .../python/pickleball/pyproject.toml | 8 + .../python/polymarket-research/.env.example | 1 + .../python/polymarket-research/README.md | 66 ++ .../python/polymarket-research/main.py | 91 +++ .../python/polymarket-research/pyproject.toml | 8 + .../python/proxies-weather/README.md | 58 ++ .../templates/python/proxies-weather/main.py | 136 +++++ .../python/proxies-weather/pyproject.toml | 8 + .../python/proxies-weather/requirements.txt | 3 + .../templates/python/proxies/.env.example | 1 + .../templates/python/proxies/README.md | 61 ++ .../examples/templates/python/proxies/main.py | 84 +++ .../templates/python/proxies/pyproject.toml | 8 + .../python/sec-filing-research/.env.example | 3 + .../python/sec-filing-research/README.md | 95 +++ .../python/sec-filing-research/main.py | 123 ++++ .../python/sec-filing-research/pyproject.toml | 25 + .../python/smart-fetch-scraper/.env.example | 2 + .../python/smart-fetch-scraper/README.md | 66 ++ .../python/smart-fetch-scraper/main.py | 246 ++++++++ .../python/smart-fetch-scraper/pyproject.toml | 25 + .../python/website-link-tester/.env.example | 2 + .../python/website-link-tester/README.md | 110 ++++ .../python/website-link-tester/main.py | 199 ++++++ .../python/website-link-tester/pyproject.toml | 8 + .../.env.example | 2 + .../amazon-global-price-comparison/README.md | 66 ++ .../amazon-global-price-comparison/index.ts | 287 +++++++++ .../package.json | 22 + .../amazon-product-scraping/.env.example | 2 + .../amazon-product-scraping/README.md | 60 ++ .../amazon-product-scraping/index.ts | 112 ++++ .../amazon-product-scraping/package.json | 25 + .../typescript/basic-caching/.env.example | 1 + .../typescript/basic-caching/README.md | 115 ++++ .../typescript/basic-caching/index.ts | 65 ++ .../typescript/basic-caching/package.json | 23 + .../typescript/basic-recaptcha/.env.example | 1 + .../typescript/basic-recaptcha/README.md | 112 ++++ .../typescript/basic-recaptcha/index.ts | 90 +++ .../typescript/basic-recaptcha/package.json | 22 + .../browserbase-reducto/.env.example | 5 + .../typescript/browserbase-reducto/README.md | 62 ++ .../typescript/browserbase-reducto/index.ts | 348 +++++++++++ .../browserbase-reducto/package.json | 27 + .../company-value-prop-generator/.env.example | 1 + .../company-value-prop-generator/README.md | 60 ++ .../company-value-prop-generator/index.ts | 100 +++ .../company-value-prop-generator/package.json | 22 + .../templates/typescript/context/.env.example | 3 + .../templates/typescript/context/README.md | 59 ++ .../templates/typescript/context/index.ts | 149 +++++ .../templates/typescript/context/package.json | 27 + .../typescript/council-events/.env.example | 1 + .../typescript/council-events/README.md | 65 ++ .../typescript/council-events/index.ts | 95 +++ .../typescript/council-events/package.json | 22 + .../.env.example | 1 + .../download-financial-statements/README.md | 64 ++ .../download-financial-statements/index.ts | 158 +++++ .../package.json | 26 + .../extend-browserbase/.env.example | 5 + .../typescript/extend-browserbase/README.md | 74 +++ .../typescript/extend-browserbase/index.ts | 459 ++++++++++++++ .../extend-browserbase/package.json | 27 + .../typescript/form-filling/.env.example | 1 + .../typescript/form-filling/README.md | 58 ++ .../typescript/form-filling/index.ts | 104 ++++ .../typescript/form-filling/package.json | 21 + .../typescript/gift-finder/.env.example | 4 + .../typescript/gift-finder/README.md | 62 ++ .../templates/typescript/gift-finder/index.ts | 393 ++++++++++++ .../typescript/gift-finder/package.json | 25 + .../typescript/google-trends/.env.example | 2 + .../typescript/google-trends/README.md | 61 ++ .../typescript/google-trends/index.ts | 109 ++++ .../typescript/google-trends/package.json | 36 ++ .../image-url-download/.env.example | 5 + .../typescript/image-url-download/.gitignore | 4 + .../typescript/image-url-download/README.md | 72 +++ .../typescript/image-url-download/index.ts | 245 ++++++++ .../image-url-download/package.json | 26 + .../typescript/job-application/.env.example | 2 + .../typescript/job-application/README.md | 83 +++ .../typescript/job-application/index.ts | 235 +++++++ .../typescript/job-application/package.json | 26 + .../manual-mfa-with-contexts/.env.example | 3 + .../manual-mfa-with-contexts/README.md | 65 ++ .../manual-mfa-with-contexts/index.ts | 233 +++++++ .../manual-mfa-with-contexts/package.json | 26 + .../typescript/mfa-handling/.env.example | 1 + .../typescript/mfa-handling/README.md | 72 +++ .../typescript/mfa-handling/index.ts | 206 +++++++ .../typescript/mfa-handling/package.json | 35 ++ .../typescript/pickleball/.env.example | 4 + .../templates/typescript/pickleball/README.md | 81 +++ .../templates/typescript/pickleball/index.ts | 467 ++++++++++++++ .../typescript/pickleball/package.json | 27 + .../polymarket-research/.env.example | 1 + .../typescript/polymarket-research/README.md | 60 ++ .../typescript/polymarket-research/index.ts | 98 +++ .../polymarket-research/package.json | 22 + .../typescript/proxies-weather/.env.example | 1 + .../typescript/proxies-weather/README.md | 53 ++ .../typescript/proxies-weather/index.ts | 220 +++++++ .../typescript/proxies-weather/package.json | 22 + .../templates/typescript/proxies/.env.example | 1 + .../templates/typescript/proxies/README.md | 52 ++ .../templates/typescript/proxies/index.ts | 101 +++ .../templates/typescript/proxies/package.json | 22 + .../sec-filing-research/.env.example | 3 + .../typescript/sec-filing-research/README.md | 66 ++ .../typescript/sec-filing-research/index.ts | 180 ++++++ .../sec-filing-research/package.json | 25 + .../smart-fetch-scraper/.env.example | 2 + .../typescript/smart-fetch-scraper/.gitignore | 3 + .../typescript/smart-fetch-scraper/README.md | 78 +++ .../typescript/smart-fetch-scraper/index.ts | 226 +++++++ .../smart-fetch-scraper/package.json | 26 + .../website-link-tester/.env.example | 3 + .../typescript/website-link-tester/README.md | 104 ++++ .../typescript/website-link-tester/index.ts | 381 ++++++++++++ .../website-link-tester/package.json | 22 + packages/skills/README.md | 9 + 382 files changed, 27392 insertions(+) create mode 100644 packages/examples/.gitignore create mode 100644 packages/examples/README.md create mode 100644 packages/examples/demos/caching-with-variables/.env.example create mode 100644 packages/examples/demos/caching-with-variables/.gitignore create mode 100644 packages/examples/demos/caching-with-variables/README.md create mode 100644 packages/examples/demos/caching-with-variables/package.json create mode 100644 packages/examples/demos/caching-with-variables/src/01-act-cache-with-variables.ts create mode 100644 packages/examples/demos/caching-with-variables/src/02-agent-cache-test.ts create mode 100644 packages/examples/demos/caching-with-variables/src/act-cache-demo.ts create mode 100644 packages/examples/demos/caching-with-variables/tsconfig.json create mode 100644 packages/examples/demos/company-news-function/.browserbase/functions/manifests/company-news-finder.json create mode 100644 packages/examples/demos/company-news-function/.env.example create mode 100644 packages/examples/demos/company-news-function/.gitignore create mode 100644 packages/examples/demos/company-news-function/README.md create mode 100644 packages/examples/demos/company-news-function/index.ts create mode 100644 packages/examples/demos/company-news-function/package.json create mode 100644 packages/examples/demos/company-news-function/tsconfig.json create mode 100644 packages/examples/demos/configurable-browser-trial/.env.example create mode 100644 packages/examples/demos/configurable-browser-trial/.gitignore create mode 100644 packages/examples/demos/configurable-browser-trial/README.md create mode 100644 packages/examples/demos/configurable-browser-trial/bin/bbpoc.mjs create mode 100644 packages/examples/demos/configurable-browser-trial/examples/trial.example.yaml create mode 100644 packages/examples/demos/configurable-browser-trial/package.json create mode 100644 packages/examples/demos/configurable-browser-trial/src/classify.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/cli.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/config.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/pool.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/report/html.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/report/markdown.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/runner.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/scorecard.ts create mode 100644 packages/examples/demos/configurable-browser-trial/src/types.ts create mode 100644 packages/examples/demos/configurable-browser-trial/tsconfig.json create mode 100644 packages/examples/demos/hacker-news-intelligence/.env.example create mode 100644 packages/examples/demos/hacker-news-intelligence/.gitignore create mode 100644 packages/examples/demos/hacker-news-intelligence/README.md create mode 100644 packages/examples/demos/hacker-news-intelligence/package.json create mode 100644 packages/examples/demos/hacker-news-intelligence/src/config.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/src/extractor.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/src/logger.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/src/main.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/src/reporter.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/src/test.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/src/types.ts create mode 100644 packages/examples/demos/hacker-news-intelligence/tsconfig.json create mode 100644 packages/examples/demos/qa-agent/README.md create mode 100644 packages/examples/demos/qa-agent/buggy-app/next-env.d.ts create mode 100644 packages/examples/demos/qa-agent/buggy-app/next.config.js create mode 100644 packages/examples/demos/qa-agent/buggy-app/package.json create mode 100644 packages/examples/demos/qa-agent/buggy-app/postcss.config.js create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/about/page.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/cart/page.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/checkout/page.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/globals.css create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/layout.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/page.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/app/product/[id]/page.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/components/CartSummary.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/components/CheckoutForm.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/components/Navbar.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/components/ProductCard.tsx create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/lib/cart-store.ts create mode 100644 packages/examples/demos/qa-agent/buggy-app/src/lib/products.ts create mode 100644 packages/examples/demos/qa-agent/buggy-app/tailwind.config.js create mode 100644 packages/examples/demos/qa-agent/buggy-app/tsconfig.json create mode 100644 packages/examples/demos/qa-agent/qa-agent/.env.example create mode 100644 packages/examples/demos/qa-agent/qa-agent/package.json create mode 100644 packages/examples/demos/qa-agent/qa-agent/src/approach-a/run.ts create mode 100644 packages/examples/demos/qa-agent/qa-agent/src/approach-a/tools.ts create mode 100644 packages/examples/demos/qa-agent/qa-agent/src/approach-b/run.ts create mode 100644 packages/examples/demos/qa-agent/qa-agent/src/index.ts create mode 100644 packages/examples/demos/qa-agent/qa-agent/src/shared/stagehand-init.ts create mode 100644 packages/examples/demos/qa-agent/qa-agent/src/shared/types.ts create mode 100644 packages/examples/demos/qa-agent/qa-agent/tsconfig.json create mode 100644 packages/examples/demos/v4-demo-kit/.env.example create mode 100644 packages/examples/demos/v4-demo-kit/.gitignore create mode 100644 packages/examples/demos/v4-demo-kit/README.md create mode 100644 packages/examples/demos/v4-demo-kit/package.json create mode 100644 packages/examples/demos/v4-demo-kit/scripts/research-hacker-news.mjs create mode 100644 packages/examples/demos/v4-demo-kit/src/agent-harness.mjs create mode 100644 packages/examples/demos/v4-demo-kit/src/cli.mjs create mode 100644 packages/examples/demos/v4-demo-kit/src/http-server.mjs create mode 100644 packages/examples/demos/v4-demo-kit/src/session.mjs create mode 100644 packages/examples/demos/v4-demo-kit/src/tools.mjs create mode 100644 packages/examples/demos/v4-demo-kit/test/tools.test.mjs create mode 100644 packages/examples/demos/web-performance/.env.example create mode 100644 packages/examples/demos/web-performance/README.md create mode 100644 packages/examples/demos/web-performance/package.json create mode 100644 packages/examples/demos/web-performance/src/perf-vitals-agent.ts create mode 100644 packages/examples/demos/web-performance/tsconfig.json create mode 100644 packages/examples/integrations/LICENSE create mode 100644 packages/examples/integrations/agentkit/README.md create mode 100644 packages/examples/integrations/agentkit/package.json create mode 100644 packages/examples/integrations/agentkit/src/index.ts create mode 100644 packages/examples/integrations/agentkit/src/stagehand-tools.ts create mode 100644 packages/examples/integrations/agentkit/src/utils.ts create mode 100644 packages/examples/integrations/agentkit/tsconfig.json create mode 100644 packages/examples/integrations/box/.env.example create mode 100644 packages/examples/integrations/box/README.md create mode 100644 packages/examples/integrations/box/package.json create mode 100644 packages/examples/integrations/box/src/box.ts create mode 100644 packages/examples/integrations/box/src/browserbase.ts create mode 100644 packages/examples/integrations/box/src/compliance.ts create mode 100644 packages/examples/integrations/box/src/index.ts create mode 100644 packages/examples/integrations/box/tsconfig.json create mode 100644 packages/examples/integrations/convex/.gitignore create mode 100644 packages/examples/integrations/convex/README.md create mode 100644 packages/examples/integrations/convex/convex/convex.config.ts create mode 100644 packages/examples/integrations/convex/convex/example.ts create mode 100644 packages/examples/integrations/convex/convex/schema.ts create mode 100644 packages/examples/integrations/convex/package.json create mode 100644 packages/examples/integrations/convex/tsconfig.json create mode 100644 packages/examples/integrations/crewai/.env.example create mode 100644 packages/examples/integrations/crewai/.gitignore create mode 100644 packages/examples/integrations/crewai/README.md create mode 100644 packages/examples/integrations/crewai/main.py create mode 100644 packages/examples/integrations/crewai/requirements.txt create mode 100644 packages/examples/integrations/deepagents/README.md create mode 100644 packages/examples/integrations/deepagents/browser_tools.py create mode 100644 packages/examples/integrations/deepagents/main.py create mode 100644 packages/examples/integrations/deepagents/requirements.txt create mode 100644 packages/examples/integrations/langchain/README.md create mode 100644 packages/examples/integrations/langchain/package.json create mode 100644 packages/examples/integrations/langchain/src/index.js create mode 100644 packages/examples/integrations/mastra/.gitignore create mode 100644 packages/examples/integrations/mastra/README.md create mode 100644 packages/examples/integrations/mastra/package.json create mode 100644 packages/examples/integrations/mastra/src/mastra/agents/index.ts create mode 100644 packages/examples/integrations/mastra/src/mastra/index.ts create mode 100644 packages/examples/integrations/mastra/src/mastra/tools/index.ts create mode 100644 packages/examples/integrations/mastra/tsconfig.json create mode 100644 packages/examples/integrations/mongodb/README.md create mode 100644 packages/examples/integrations/mongodb/python/.gitignore create mode 100644 packages/examples/integrations/mongodb/python/README.md create mode 100644 packages/examples/integrations/mongodb/python/env.example create mode 100644 packages/examples/integrations/mongodb/python/main.py create mode 100644 packages/examples/integrations/mongodb/python/requirements.txt create mode 100644 packages/examples/integrations/mongodb/typescript/.env.example create mode 100644 packages/examples/integrations/mongodb/typescript/.gitignore create mode 100644 packages/examples/integrations/mongodb/typescript/LICENSE create mode 100644 packages/examples/integrations/mongodb/typescript/README.md create mode 100644 packages/examples/integrations/mongodb/typescript/index.ts create mode 100644 packages/examples/integrations/mongodb/typescript/package.json create mode 100644 packages/examples/integrations/mongodb/typescript/tsconfig.json create mode 100644 packages/examples/integrations/mongodb/typescript/utils.ts create mode 100644 packages/examples/integrations/temporal/.env.example create mode 100644 packages/examples/integrations/temporal/.eslintignore create mode 100644 packages/examples/integrations/temporal/.eslintrc.js create mode 100644 packages/examples/integrations/temporal/.gitignore create mode 100644 packages/examples/integrations/temporal/.nvmrc create mode 100644 packages/examples/integrations/temporal/.prettierrc create mode 100644 packages/examples/integrations/temporal/README.md create mode 100644 packages/examples/integrations/temporal/jest.config.js create mode 100644 packages/examples/integrations/temporal/package.json create mode 100644 packages/examples/integrations/temporal/src/demo.ts create mode 100644 packages/examples/integrations/temporal/src/research-activities.ts create mode 100644 packages/examples/integrations/temporal/src/research-worker.ts create mode 100644 packages/examples/integrations/temporal/src/workflows.ts create mode 100644 packages/examples/integrations/temporal/tsconfig.json create mode 100644 packages/examples/oxlint.config.ts create mode 100644 packages/examples/playbook/1password-extension/.env.example create mode 100644 packages/examples/playbook/1password-extension/README.md create mode 100644 packages/examples/playbook/1password-extension/node/createExtension.ts create mode 100644 packages/examples/playbook/1password-extension/node/stagehand.ts create mode 100644 packages/examples/playbook/1password-extension/package.json create mode 100644 packages/examples/playbook/1password-extension/python/create_extension.py create mode 100644 packages/examples/playbook/1password-extension/python/run_stagehand.py create mode 100644 packages/examples/playbook/alaska-flights/.env.example create mode 100644 packages/examples/playbook/alaska-flights/README.md create mode 100644 packages/examples/playbook/alaska-flights/package.json create mode 100644 packages/examples/playbook/alaska-flights/searchAlaskaFlights.ts create mode 100644 packages/examples/playbook/docs-search/.env.example create mode 100644 packages/examples/playbook/docs-search/README.md create mode 100644 packages/examples/playbook/docs-search/package.json create mode 100644 packages/examples/playbook/docs-search/searchDocs.ts create mode 100644 packages/examples/playbook/hacker-news/.env.example create mode 100644 packages/examples/playbook/hacker-news/README.md create mode 100644 packages/examples/playbook/hacker-news/package.json create mode 100644 packages/examples/playbook/hacker-news/pullNews.ts create mode 100644 packages/examples/playbook/southwest-flights/.env.example create mode 100644 packages/examples/playbook/southwest-flights/README.md create mode 100644 packages/examples/playbook/southwest-flights/package.json create mode 100644 packages/examples/playbook/southwest-flights/searchSouthwestFlights.ts create mode 100644 packages/examples/templates/python/amazon-global-price-comparison/.env.example create mode 100644 packages/examples/templates/python/amazon-global-price-comparison/README.md create mode 100644 packages/examples/templates/python/amazon-global-price-comparison/main.py create mode 100644 packages/examples/templates/python/amazon-global-price-comparison/pyproject.toml create mode 100644 packages/examples/templates/python/amazon-product-scraping/.env.example create mode 100644 packages/examples/templates/python/amazon-product-scraping/README.md create mode 100644 packages/examples/templates/python/amazon-product-scraping/main.py create mode 100644 packages/examples/templates/python/amazon-product-scraping/pyproject.toml create mode 100644 packages/examples/templates/python/basic-caching/.env.example create mode 100644 packages/examples/templates/python/basic-caching/README.md create mode 100644 packages/examples/templates/python/basic-caching/main.py create mode 100644 packages/examples/templates/python/basic-caching/pyproject.toml create mode 100644 packages/examples/templates/python/basic-caching/requirements.txt create mode 100644 packages/examples/templates/python/basic-recaptcha/.env.example create mode 100644 packages/examples/templates/python/basic-recaptcha/README.md create mode 100644 packages/examples/templates/python/basic-recaptcha/main.py create mode 100644 packages/examples/templates/python/basic-recaptcha/pyproject.toml create mode 100644 packages/examples/templates/python/browserbase-reducto/README.md create mode 100644 packages/examples/templates/python/browserbase-reducto/main.py create mode 100644 packages/examples/templates/python/browserbase-reducto/pyproject.toml create mode 100644 packages/examples/templates/python/company-value-prop-generator/.env.example create mode 100644 packages/examples/templates/python/company-value-prop-generator/README.md create mode 100644 packages/examples/templates/python/company-value-prop-generator/main.py create mode 100644 packages/examples/templates/python/company-value-prop-generator/pyproject.toml create mode 100644 packages/examples/templates/python/context/.env.example create mode 100644 packages/examples/templates/python/context/README.md create mode 100644 packages/examples/templates/python/context/main.py create mode 100644 packages/examples/templates/python/context/pyproject.toml create mode 100644 packages/examples/templates/python/council-events/.env.example create mode 100644 packages/examples/templates/python/council-events/README.md create mode 100644 packages/examples/templates/python/council-events/main.py create mode 100644 packages/examples/templates/python/council-events/pyproject.toml create mode 100644 packages/examples/templates/python/download-financial-statements/.env.example create mode 100644 packages/examples/templates/python/download-financial-statements/README.md create mode 100644 packages/examples/templates/python/download-financial-statements/main.py create mode 100644 packages/examples/templates/python/download-financial-statements/pyproject.toml create mode 100644 packages/examples/templates/python/extend-browserbase/.env.example create mode 100644 packages/examples/templates/python/extend-browserbase/README.md create mode 100644 packages/examples/templates/python/extend-browserbase/main.py create mode 100644 packages/examples/templates/python/extend-browserbase/pyproject.toml create mode 100644 packages/examples/templates/python/form-filling/.env.example create mode 100644 packages/examples/templates/python/form-filling/README.md create mode 100644 packages/examples/templates/python/form-filling/main.py create mode 100644 packages/examples/templates/python/form-filling/pyproject.toml create mode 100644 packages/examples/templates/python/gift-finder/.env.example create mode 100644 packages/examples/templates/python/gift-finder/README.md create mode 100644 packages/examples/templates/python/gift-finder/main.py create mode 100644 packages/examples/templates/python/gift-finder/pyproject.toml create mode 100644 packages/examples/templates/python/google-trends/.env.example create mode 100644 packages/examples/templates/python/google-trends/README.md create mode 100644 packages/examples/templates/python/google-trends/main.py create mode 100644 packages/examples/templates/python/google-trends/pyproject.toml create mode 100644 packages/examples/templates/python/image-url-download/.env.example create mode 100644 packages/examples/templates/python/image-url-download/.gitignore create mode 100644 packages/examples/templates/python/image-url-download/README.md create mode 100644 packages/examples/templates/python/image-url-download/main.py create mode 100644 packages/examples/templates/python/image-url-download/pyproject.toml create mode 100644 packages/examples/templates/python/job-application/.env.example create mode 100644 packages/examples/templates/python/job-application/README.md create mode 100644 packages/examples/templates/python/job-application/main.py create mode 100644 packages/examples/templates/python/job-application/pyproject.toml create mode 100644 packages/examples/templates/python/manual-mfa-with-contexts/.env.example create mode 100644 packages/examples/templates/python/manual-mfa-with-contexts/README.md create mode 100644 packages/examples/templates/python/manual-mfa-with-contexts/main.py create mode 100644 packages/examples/templates/python/manual-mfa-with-contexts/pyproject.toml create mode 100644 packages/examples/templates/python/manual-mfa-with-contexts/requirements.txt create mode 100644 packages/examples/templates/python/mfa-handling/.env.example create mode 100644 packages/examples/templates/python/mfa-handling/README.md create mode 100644 packages/examples/templates/python/mfa-handling/main.py create mode 100644 packages/examples/templates/python/mfa-handling/pyproject.toml create mode 100644 packages/examples/templates/python/mfa-handling/requirements.txt create mode 100644 packages/examples/templates/python/pickleball/.env.example create mode 100644 packages/examples/templates/python/pickleball/README.md create mode 100644 packages/examples/templates/python/pickleball/main.py create mode 100644 packages/examples/templates/python/pickleball/pyproject.toml create mode 100644 packages/examples/templates/python/polymarket-research/.env.example create mode 100644 packages/examples/templates/python/polymarket-research/README.md create mode 100644 packages/examples/templates/python/polymarket-research/main.py create mode 100644 packages/examples/templates/python/polymarket-research/pyproject.toml create mode 100644 packages/examples/templates/python/proxies-weather/README.md create mode 100644 packages/examples/templates/python/proxies-weather/main.py create mode 100644 packages/examples/templates/python/proxies-weather/pyproject.toml create mode 100644 packages/examples/templates/python/proxies-weather/requirements.txt create mode 100644 packages/examples/templates/python/proxies/.env.example create mode 100644 packages/examples/templates/python/proxies/README.md create mode 100644 packages/examples/templates/python/proxies/main.py create mode 100644 packages/examples/templates/python/proxies/pyproject.toml create mode 100644 packages/examples/templates/python/sec-filing-research/.env.example create mode 100644 packages/examples/templates/python/sec-filing-research/README.md create mode 100644 packages/examples/templates/python/sec-filing-research/main.py create mode 100644 packages/examples/templates/python/sec-filing-research/pyproject.toml create mode 100644 packages/examples/templates/python/smart-fetch-scraper/.env.example create mode 100644 packages/examples/templates/python/smart-fetch-scraper/README.md create mode 100644 packages/examples/templates/python/smart-fetch-scraper/main.py create mode 100644 packages/examples/templates/python/smart-fetch-scraper/pyproject.toml create mode 100644 packages/examples/templates/python/website-link-tester/.env.example create mode 100644 packages/examples/templates/python/website-link-tester/README.md create mode 100644 packages/examples/templates/python/website-link-tester/main.py create mode 100644 packages/examples/templates/python/website-link-tester/pyproject.toml create mode 100644 packages/examples/templates/typescript/amazon-global-price-comparison/.env.example create mode 100644 packages/examples/templates/typescript/amazon-global-price-comparison/README.md create mode 100644 packages/examples/templates/typescript/amazon-global-price-comparison/index.ts create mode 100644 packages/examples/templates/typescript/amazon-global-price-comparison/package.json create mode 100644 packages/examples/templates/typescript/amazon-product-scraping/.env.example create mode 100644 packages/examples/templates/typescript/amazon-product-scraping/README.md create mode 100644 packages/examples/templates/typescript/amazon-product-scraping/index.ts create mode 100644 packages/examples/templates/typescript/amazon-product-scraping/package.json create mode 100644 packages/examples/templates/typescript/basic-caching/.env.example create mode 100644 packages/examples/templates/typescript/basic-caching/README.md create mode 100644 packages/examples/templates/typescript/basic-caching/index.ts create mode 100644 packages/examples/templates/typescript/basic-caching/package.json create mode 100644 packages/examples/templates/typescript/basic-recaptcha/.env.example create mode 100644 packages/examples/templates/typescript/basic-recaptcha/README.md create mode 100644 packages/examples/templates/typescript/basic-recaptcha/index.ts create mode 100644 packages/examples/templates/typescript/basic-recaptcha/package.json create mode 100644 packages/examples/templates/typescript/browserbase-reducto/.env.example create mode 100644 packages/examples/templates/typescript/browserbase-reducto/README.md create mode 100644 packages/examples/templates/typescript/browserbase-reducto/index.ts create mode 100644 packages/examples/templates/typescript/browserbase-reducto/package.json create mode 100644 packages/examples/templates/typescript/company-value-prop-generator/.env.example create mode 100644 packages/examples/templates/typescript/company-value-prop-generator/README.md create mode 100644 packages/examples/templates/typescript/company-value-prop-generator/index.ts create mode 100644 packages/examples/templates/typescript/company-value-prop-generator/package.json create mode 100644 packages/examples/templates/typescript/context/.env.example create mode 100644 packages/examples/templates/typescript/context/README.md create mode 100644 packages/examples/templates/typescript/context/index.ts create mode 100644 packages/examples/templates/typescript/context/package.json create mode 100644 packages/examples/templates/typescript/council-events/.env.example create mode 100644 packages/examples/templates/typescript/council-events/README.md create mode 100644 packages/examples/templates/typescript/council-events/index.ts create mode 100644 packages/examples/templates/typescript/council-events/package.json create mode 100644 packages/examples/templates/typescript/download-financial-statements/.env.example create mode 100644 packages/examples/templates/typescript/download-financial-statements/README.md create mode 100644 packages/examples/templates/typescript/download-financial-statements/index.ts create mode 100644 packages/examples/templates/typescript/download-financial-statements/package.json create mode 100644 packages/examples/templates/typescript/extend-browserbase/.env.example create mode 100644 packages/examples/templates/typescript/extend-browserbase/README.md create mode 100644 packages/examples/templates/typescript/extend-browserbase/index.ts create mode 100644 packages/examples/templates/typescript/extend-browserbase/package.json create mode 100644 packages/examples/templates/typescript/form-filling/.env.example create mode 100644 packages/examples/templates/typescript/form-filling/README.md create mode 100644 packages/examples/templates/typescript/form-filling/index.ts create mode 100644 packages/examples/templates/typescript/form-filling/package.json create mode 100644 packages/examples/templates/typescript/gift-finder/.env.example create mode 100644 packages/examples/templates/typescript/gift-finder/README.md create mode 100644 packages/examples/templates/typescript/gift-finder/index.ts create mode 100644 packages/examples/templates/typescript/gift-finder/package.json create mode 100644 packages/examples/templates/typescript/google-trends/.env.example create mode 100644 packages/examples/templates/typescript/google-trends/README.md create mode 100644 packages/examples/templates/typescript/google-trends/index.ts create mode 100644 packages/examples/templates/typescript/google-trends/package.json create mode 100644 packages/examples/templates/typescript/image-url-download/.env.example create mode 100644 packages/examples/templates/typescript/image-url-download/.gitignore create mode 100644 packages/examples/templates/typescript/image-url-download/README.md create mode 100644 packages/examples/templates/typescript/image-url-download/index.ts create mode 100644 packages/examples/templates/typescript/image-url-download/package.json create mode 100644 packages/examples/templates/typescript/job-application/.env.example create mode 100644 packages/examples/templates/typescript/job-application/README.md create mode 100644 packages/examples/templates/typescript/job-application/index.ts create mode 100644 packages/examples/templates/typescript/job-application/package.json create mode 100644 packages/examples/templates/typescript/manual-mfa-with-contexts/.env.example create mode 100644 packages/examples/templates/typescript/manual-mfa-with-contexts/README.md create mode 100644 packages/examples/templates/typescript/manual-mfa-with-contexts/index.ts create mode 100644 packages/examples/templates/typescript/manual-mfa-with-contexts/package.json create mode 100644 packages/examples/templates/typescript/mfa-handling/.env.example create mode 100644 packages/examples/templates/typescript/mfa-handling/README.md create mode 100644 packages/examples/templates/typescript/mfa-handling/index.ts create mode 100644 packages/examples/templates/typescript/mfa-handling/package.json create mode 100644 packages/examples/templates/typescript/pickleball/.env.example create mode 100644 packages/examples/templates/typescript/pickleball/README.md create mode 100644 packages/examples/templates/typescript/pickleball/index.ts create mode 100644 packages/examples/templates/typescript/pickleball/package.json create mode 100644 packages/examples/templates/typescript/polymarket-research/.env.example create mode 100644 packages/examples/templates/typescript/polymarket-research/README.md create mode 100644 packages/examples/templates/typescript/polymarket-research/index.ts create mode 100644 packages/examples/templates/typescript/polymarket-research/package.json create mode 100644 packages/examples/templates/typescript/proxies-weather/.env.example create mode 100644 packages/examples/templates/typescript/proxies-weather/README.md create mode 100644 packages/examples/templates/typescript/proxies-weather/index.ts create mode 100644 packages/examples/templates/typescript/proxies-weather/package.json create mode 100644 packages/examples/templates/typescript/proxies/.env.example create mode 100644 packages/examples/templates/typescript/proxies/README.md create mode 100644 packages/examples/templates/typescript/proxies/index.ts create mode 100644 packages/examples/templates/typescript/proxies/package.json create mode 100644 packages/examples/templates/typescript/sec-filing-research/.env.example create mode 100644 packages/examples/templates/typescript/sec-filing-research/README.md create mode 100644 packages/examples/templates/typescript/sec-filing-research/index.ts create mode 100644 packages/examples/templates/typescript/sec-filing-research/package.json create mode 100644 packages/examples/templates/typescript/smart-fetch-scraper/.env.example create mode 100644 packages/examples/templates/typescript/smart-fetch-scraper/.gitignore create mode 100644 packages/examples/templates/typescript/smart-fetch-scraper/README.md create mode 100644 packages/examples/templates/typescript/smart-fetch-scraper/index.ts create mode 100644 packages/examples/templates/typescript/smart-fetch-scraper/package.json create mode 100644 packages/examples/templates/typescript/website-link-tester/.env.example create mode 100644 packages/examples/templates/typescript/website-link-tester/README.md create mode 100644 packages/examples/templates/typescript/website-link-tester/index.ts create mode 100644 packages/examples/templates/typescript/website-link-tester/package.json create mode 100644 packages/skills/README.md diff --git a/oxlint.config.ts b/oxlint.config.ts index 2faec66b6c..5b46af7a21 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -2,6 +2,9 @@ import { defineConfig } from "oxlint"; import { stagehandRuleConfig } from "./rules/oxlint/stagehand-plugin.ts"; export default defineConfig({ + // These standalone projects retain their own SDK generations and dependencies. + // Lint them with packages/examples/oxlint.config.ts after copying or editing. + ignorePatterns: ["packages/examples/**"], jsPlugins: [{ name: "stagehand", specifier: "./rules/oxlint/stagehand-plugin.ts" }], rules: { "no-console": "error", diff --git a/packages/examples/.gitignore b/packages/examples/.gitignore new file mode 100644 index 0000000000..0265667eb5 --- /dev/null +++ b/packages/examples/.gitignore @@ -0,0 +1,13 @@ +# Local runtime output from standalone examples +.cache/ +__pycache__/ +.venv/ +.next/ +.convex/ +reports/ +screenshots/ +output/ +outputs/ +downloads/ +downloaded_files.zip +*.tsbuildinfo diff --git a/packages/examples/README.md b/packages/examples/README.md new file mode 100644 index 0000000000..8621d442fb --- /dev/null +++ b/packages/examples/README.md @@ -0,0 +1,95 @@ +# Stagehand examples + +Standalone examples grouped by source and runtime. Each example keeps its own dependencies and setup instructions; nested projects are intentionally outside the root pnpm workspace. + +## Start here + +- [Basic caching (TypeScript)](templates/typescript/basic-caching/README.md) and [Python](templates/python/basic-caching/README.md): small Stagehand v4 examples. +- [v4 demo kit](demos/v4-demo-kit/README.md): an agent harness, reusable tools, mixed browser/AI control, and an HTTP runner. +- [Skills](../skills/README.md): the Browserbase skill collections maintained in their own repository. + +## Run an example + +Enter an individual example directory and follow its README. TypeScript projects generally use `npm install` and their listed script; Python projects use `uv sync` or their requirements file. Supply credentials through your environment. The `.env.example` files list configuration inputs. + +These are source examples spanning several SDK generations. The templates use Stagehand v4; older playbooks and some integrations/demos retain v2/v3 APIs. Check the version column and local dependency manifest before running them. Moving the examples does not migrate their APIs. Live sites, external service setup, and retired model names may need updates. + +Examples that authenticate, submit demo forms, book recreation slots, write to databases, or upload files describe those actions in their individual READMEs. No saved browser state, customer records, credentials, or generated session output is bundled. + +## Catalog + +| Example | Stagehand dependency | Purpose | +| -------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [templates/typescript/amazon-global-price-comparison](templates/typescript/amazon-global-price-comparison/README.md) | 4.0.0 | Public product search across storefronts with configurable product input; no customer account data. | +| [templates/python/amazon-global-price-comparison](templates/python/amazon-global-price-comparison/README.md) | 4.0.0 | Public product search across storefronts with configurable product input; no customer account data. | +| [templates/typescript/amazon-product-scraping](templates/typescript/amazon-product-scraping/README.md) | 4.0.0 | Structured extraction from public product search results. | +| [templates/python/amazon-product-scraping](templates/python/amazon-product-scraping/README.md) | 4.0.0 | Structured extraction from public product search results. | +| [templates/typescript/basic-caching](templates/typescript/basic-caching/README.md) | 4.0.2 | Observe/cache example on the reserved example.com domain. | +| [templates/python/basic-caching](templates/python/basic-caching/README.md) | 4.0.0 | Observe/cache example on the reserved example.com domain. | +| [templates/typescript/basic-recaptcha](templates/typescript/basic-recaptcha/README.md) | 4.0.2 | Stagehand actions against the public reCAPTCHA demo. | +| [templates/python/basic-recaptcha](templates/python/basic-recaptcha/README.md) | 4.0.0 | Stagehand actions against the public reCAPTCHA demo. | +| [templates/typescript/browserbase-reducto](templates/typescript/browserbase-reducto/README.md) | 4.0.0 | Public Apple investor documents downloaded with Stagehand and parsed with Reducto; keys come from the environment. | +| [templates/python/browserbase-reducto](templates/python/browserbase-reducto/README.md) | 4.0.0 | Public Apple investor documents downloaded with Stagehand and parsed with Reducto; keys come from the environment. | +| [templates/typescript/company-value-prop-generator](templates/typescript/company-value-prop-generator/README.md) | 4.0.2 | Configurable public-company website extraction; no customer tenant or account. | +| [templates/python/company-value-prop-generator](templates/python/company-value-prop-generator/README.md) | 4.0.0 | Configurable public-company website extraction; no customer tenant or account. | +| [templates/typescript/context](templates/typescript/context/README.md) | 4.0.0 | Public recreation-site context reuse; login values are supplied at runtime, with no bundled account state. | +| [templates/python/context](templates/python/context/README.md) | 4.0.0 | Public recreation-site context reuse; login values are supplied at runtime, with no bundled account state. | +| [templates/typescript/council-events](templates/typescript/council-events/README.md) | 4.0.2 | Read-only extraction of public Philadelphia council calendar records. | +| [templates/python/council-events](templates/python/council-events/README.md) | 4.0.0 | Read-only extraction of public Philadelphia council calendar records. | +| [templates/typescript/download-financial-statements](templates/typescript/download-financial-statements/README.md) | 4.0.0 | Download public Apple financial statements using Stagehand-discovered controls. | +| [templates/python/download-financial-statements](templates/python/download-financial-statements/README.md) | 4.0.0 | Download public Apple financial statements using Stagehand-discovered controls. | +| [templates/typescript/extend-browserbase](templates/typescript/extend-browserbase/README.md) | 4.0.0 | Downloads synthetic receipts from a demo expense portal and parses them with Extend. | +| [templates/python/extend-browserbase](templates/python/extend-browserbase/README.md) | 4.0.0 | Downloads synthetic receipts from a demo expense portal and parses them with Extend. | +| [templates/typescript/form-filling](templates/typescript/form-filling/README.md) | 4.0.2 | Browserbase contact-form demonstration with synthetic inputs; example email normalized to example.com; submit remains disabled. | +| [templates/python/form-filling](templates/python/form-filling/README.md) | 4.0.0 | Browserbase contact-form demonstration with synthetic inputs; example email normalized to example.com; submit remains disabled. | +| [templates/typescript/gift-finder](templates/typescript/gift-finder/README.md) | 4.0.0 | Public retail product recommendations with configurable recipient interests. | +| [templates/python/gift-finder](templates/python/gift-finder/README.md) | 4.0.0 | Public retail product recommendations with configurable recipient interests. | +| [templates/typescript/google-trends](templates/typescript/google-trends/README.md) | 4.0.0 | Read-only public trending-search extraction with country/language inputs. | +| [templates/python/google-trends](templates/python/google-trends/README.md) | 4.0.0 | Read-only public trending-search extraction with country/language inputs. | +| [templates/typescript/image-url-download](templates/typescript/image-url-download/README.md) | 4.0.0 | Find and download public website images; no captured output is included. | +| [templates/python/image-url-download](templates/python/image-url-download/README.md) | 4.0.0 | Find and download public website images; no captured output is included. | +| [templates/typescript/job-application](templates/typescript/job-application/README.md) | 4.0.0 | Dedicated agent job-board demo with generated example.com addresses and an agent resume, not a customer ATS. | +| [templates/python/job-application](templates/python/job-application/README.md) | 4.0.0 | Dedicated agent job-board demo with generated example.com addresses and an agent resume, not a customer ATS. | +| [templates/typescript/manual-mfa-with-contexts](templates/typescript/manual-mfa-with-contexts/README.md) | 4.0.0 | Generic GitHub login with interactive user authentication and fresh runtime contexts. | +| [templates/python/manual-mfa-with-contexts](templates/python/manual-mfa-with-contexts/README.md) | 4.0.0 | Generic GitHub login with interactive user authentication and fresh runtime contexts. | +| [templates/typescript/mfa-handling](templates/typescript/mfa-handling/README.md) | 4.0.0 | TOTP demonstration against a public authentication test site, with demo values discovered at runtime. | +| [templates/python/mfa-handling](templates/python/mfa-handling/README.md) | 4.0.0 | TOTP demonstration against a public authentication test site, with demo values discovered at runtime. | +| [templates/typescript/pickleball](templates/typescript/pickleball/README.md) | 4.0.0 | Public recreation-site booking example; credentials and choices are runtime inputs, no customer account fixture. | +| [templates/python/pickleball](templates/python/pickleball/README.md) | 4.0.0 | Public recreation-site booking example; credentials and choices are runtime inputs, no customer account fixture. | +| [templates/typescript/polymarket-research](templates/typescript/polymarket-research/README.md) | 4.0.2 | Read-only public prediction-market research. | +| [templates/python/polymarket-research](templates/python/polymarket-research/README.md) | 4.0.0 | Read-only public prediction-market research. | +| [templates/typescript/proxies](templates/typescript/proxies/README.md) | 4.0.2 | Stagehand extraction from public IP-information endpoints. | +| [templates/python/proxies](templates/python/proxies/README.md) | 4.0.0 | Stagehand extraction from public IP-information endpoints. | +| [templates/typescript/proxies-weather](templates/typescript/proxies-weather/README.md) | 4.0.0 | Public weather extraction with proxy geography configuration. | +| [templates/python/proxies-weather](templates/python/proxies-weather/README.md) | 4.0.0 | Public weather extraction with proxy geography configuration. | +| [templates/typescript/sec-filing-research](templates/typescript/sec-filing-research/README.md) | 4.0.0 | Public SEC filings research using sample public-company identifiers. | +| [templates/python/sec-filing-research](templates/python/sec-filing-research/README.md) | 4.0.0 | Public SEC filings research using sample public-company identifiers. | +| [templates/typescript/smart-fetch-scraper](templates/typescript/smart-fetch-scraper/README.md) | 4.0.0 | Fetch-first extraction with a meaningful Stagehand browser fallback. | +| [templates/python/smart-fetch-scraper](templates/python/smart-fetch-scraper/README.md) | 4.0.0 | Fetch-first extraction with a meaningful Stagehand browser fallback. | +| [templates/typescript/website-link-tester](templates/typescript/website-link-tester/README.md) | 4.0.2 | Public website link checking using Stagehand extraction and configurable target URL. | +| [templates/python/website-link-tester](templates/python/website-link-tester/README.md) | 4.0.0 | Public website link checking using Stagehand extraction and configurable target URL. | +| [playbook/hacker-news](playbook/hacker-news/README.md) | ^2.2.1 | Extracts public Hacker News headlines with Stagehand v2. | +| [playbook/alaska-flights](playbook/alaska-flights/README.md) | ^2.2.1 | Public flight search; sample route and dates, no passenger profile or booking. | +| [playbook/southwest-flights](playbook/southwest-flights/README.md) | ^2.2.1 | Public flight search using relative travel dates, no passenger profile or booking. | +| [playbook/docs-search](playbook/docs-search/README.md) | ^2.2.1 | Searches public Stagehand documentation and extracts the answer. | +| [playbook/1password-extension](playbook/1password-extension/README.md) | ^2.2.1 | Stagehand extension-login snippets for Node and legacy Python; credentials and extension IDs are runtime inputs, and the extension archive is excluded. | +| [integrations/agentkit](integrations/agentkit/README.md) | ^3.2.0 | Reusable Stagehand tool definitions for an Inngest agent network; user-supplied tasks. | +| [integrations/box](integrations/box/README.md) | ^3.7.1 | Downloads public regulatory/product documents with Stagehand and uploads to a runtime-configured Box folder. | +| [integrations/crewai](integrations/crewai/README.md) | 0.3.10, 0.4.0 | CrewAI StagehandTool example with reserved example.com inputs; placeholder target requires configuration. | +| [integrations/langchain](integrations/langchain/README.md) | ^2.4.4 | LangChain Stagehand toolkit demonstrated with a public web search. | +| [integrations/deepagents](integrations/deepagents/README.md) | 3.20.0 | Reusable Stagehand session/act/extract tools alongside Browserbase search/fetch. | +| [integrations/mastra](integrations/mastra/README.md) | ^3.2.0 | Mastra tool wrappers around Stagehand act, observe, extract, and navigation. | +| [integrations/mongodb](integrations/mongodb/README.md) | 0.3.0, ^4.0.0 | Public retail extraction persisted to a runtime-configured MongoDB database; Python and TypeScript variants. | +| [integrations/temporal](integrations/temporal/README.md) | ^2.4.4 | Durable web-research activities and workflow; public search and runtime research input. | +| [integrations/convex](integrations/convex/README.md) | via Convex component | Convex action wrappers for Stagehand act/extract/observe/navigation with runtime URL/action inputs; generated Convex bindings are created by convex dev. | +| [demos/caching-with-variables](demos/caching-with-variables/README.md) | 3.0.7 | Generic caching experiments against a public test login; synthetic email examples use reserved domains. | +| [demos/v4-demo-kit](demos/v4-demo-kit/README.md) | 4.0.2 | Reusable v4 tools, agent harness, scripts, and HTTP runner using example.com and Hacker News. | +| [demos/configurable-browser-trial](demos/configurable-browser-trial/README.md) | ^2.4.0 | Configurable Stagehand trial runner with public demo defaults; no customer URL list. | +| [demos/company-news-function](demos/company-news-function/README.md) | 3.0.8 | Stagehand agent in a Browserbase Function; company name is an input. | +| [demos/hacker-news-intelligence](demos/hacker-news-intelligence/README.md) | ^1.5.0 | Public Hacker News discovery and article analysis; generated reports are excluded. | +| [demos/qa-agent](demos/qa-agent/README.md) | ^2.5.2 | Reusable QA agent and intentionally buggy local storefront; no customer application. | +| [demos/web-performance](demos/web-performance/README.md) | ^3.0.0 | Stagehand agent custom tools read native browser performance metrics on public pages. | + +## Validate source changes + +From the repository root, run `pnpm exec oxlint --config packages/examples/oxlint.config.ts packages/examples` and `pnpm exec oxfmt --check packages/examples packages/skills`. Type checks and runtime tests belong to each standalone example’s own dependency installation; the main SDK lint excludes these independent projects. diff --git a/packages/examples/demos/caching-with-variables/.env.example b/packages/examples/demos/caching-with-variables/.env.example new file mode 100644 index 0000000000..e81139f0a5 --- /dev/null +++ b/packages/examples/demos/caching-with-variables/.env.example @@ -0,0 +1,9 @@ +# OpenAI API Key (required for LLM calls) +OPENAI_API_KEY= + +# Anthropic API Key (optional, for Claude models) +ANTHROPIC_API_KEY= + +# Browserbase credentials (optional, for BROWSERBASE env) +BROWSERBASE_API_KEY= +BROWSERBASE_PROJECT_ID= diff --git a/packages/examples/demos/caching-with-variables/.gitignore b/packages/examples/demos/caching-with-variables/.gitignore new file mode 100644 index 0000000000..3e7467a42e --- /dev/null +++ b/packages/examples/demos/caching-with-variables/.gitignore @@ -0,0 +1,24 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.*.local + +# Build outputs +dist/ +build/ + +# Logs +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# Claude +.claude/ diff --git a/packages/examples/demos/caching-with-variables/README.md b/packages/examples/demos/caching-with-variables/README.md new file mode 100644 index 0000000000..606b999ca9 --- /dev/null +++ b/packages/examples/demos/caching-with-variables/README.md @@ -0,0 +1,227 @@ +# Stagehand Caching with Variables Demo + +This demo showcases Stagehand's caching mechanism, specifically demonstrating: + +1. **Privacy-Preserving Caching**: Variable VALUES are NOT stored in cache, only variable KEYS +2. **Cache Efficiency**: Different variable values still hit the same cache entry +3. **Agent Caching**: Testing whether `agent()` uses caching (it does!) + +## Quick Start + +```bash +# Install dependencies +npm install + +# Copy environment variables +cp .env.example .env +# Add your OPENAI_API_KEY to .env + +# Run Demo 1: Act caching with variables +npm run demo:act-cache + +# Run Demo 2: Agent caching test +npm run demo:agent-cache + +# Run both demos +npm run demo:all +``` + +## How Stagehand Caching Works + +### Act Caching with Variables + +The key insight is that **cache keys are based on variable KEYS, not VALUES**. + +```typescript +// This is how the cache key is generated: +cacheKey = hash({ + instruction: "Type %username% into the username field", + url: "https://example.com/login", + variableKeys: ["username"], // Only KEYS, not values! +}); +``` + +This means: + +- Running with `username = "john@example.com"` creates a cache entry +- Running with `username = "secret@example.com"` **HITS THE SAME CACHE** +- The actual username value is **NEVER STORED** in the cache file + +### What's Stored in Cache + +```json +{ + "version": 1, + "instruction": "Type %username% into the username field", + "url": "https://the-internet.herokuapp.com/login", + "variableKeys": ["username"], + "actions": [ + { + "method": "fill", + "selector": "#username", + "arguments": ["%username%"] + } + ] +} +``` + +Notice: + +- The `instruction` contains the placeholder `%username%` +- The `variableKeys` array lists which variables are expected +- The `arguments` contain `%username%` placeholder, NOT the actual value +- **No actual username values in the cache file!** + +### At Replay Time + +When a cache hit occurs: + +1. Cached action is loaded (with `%username%` placeholder) +2. Current variable values are substituted +3. Action is executed with real values +4. No LLM call needed! + +## Demo 1: Act Caching with Variables + +This demo runs the same `act()` command with three different usernames: + +``` +Run 1: john.doe@example.com -> Cache MISS (LLM inference) +Run 2: john.doe@example.com -> Cache HIT (instant) +Run 3: jane.smith@example.com -> Cache HIT (still works!) +Run 4: secret.user@example.com -> Cache HIT (still works!) +``` + +**Key Takeaway**: Different usernames still hit the same cache because only the variable KEY is part of the cache key. + +## Demo 2: Agent Caching Test + +Tests whether `stagehand.agent()` uses caching: + +- **Result**: YES, agent has full caching support +- Stores entire workflow (all steps) +- Replays steps without LLM calls + +However, **agent does NOT support variables** like `act()` does. Variable data in agent instructions is stored in cache as-is. + +## Comparison Table + +| Feature | `act()` Cache | `agent()` Cache | +| -------------------- | -------------------------------- | ------------------------------------ | +| Caching supported | Yes | Yes | +| Variables supported | Yes | **No** | +| Privacy preservation | **Yes** | No | +| Multi-step workflows | Single action | Full workflow | +| Cache key based on | instruction + url + variableKeys | instruction + url + options + config | + +## Performance Benefits + +| Scenario | Time per Action | Cost | +| ---------------- | --------------- | ------------ | +| Cache MISS (LLM) | 1-3 seconds | ~$0.001-0.01 | +| Cache HIT | <100ms | $0 | + +For high-volume automation: + +- 10,000 payments × 5 actions = 50,000 actions +- Without cache: ~$500-2,500, 15+ hours +- With cache: ~$0.50 (10 cache populating runs), <1 hour + +## Privacy Use Cases + +The variable caching mechanism is perfect for: + +1. **Login Forms**: Different usernames, same cached action +2. **Payment Forms**: Different card numbers, never stored in cache +3. **Search Inputs**: Different queries, cached navigation +4. **Form Filling**: Sensitive data never persisted + +```typescript +// Example: Login with sensitive credentials +await page.act("Type %email% into the email field", { + variables: { email: "sensitive@example.com" }, +}); + +await page.act("Type %password% into the password field", { + variables: { password: "super-secret-password" }, +}); + +// Cache files will contain: +// - %email% placeholder (not the actual email) +// - %password% placeholder (not the actual password) +``` + +## Future: Agent Cache with Variables + +Currently, agent does NOT support variables. To add this functionality would require: + +1. Variable placeholder syntax in agent instructions +2. Stripping variable values from cached steps +3. Value injection during replay +4. Cache key based on variable keys (like act) + +This demo sets up the testing framework for when/if this feature is added. + +## File Structure + +``` +stagehand-caching-demo/ +├── package.json +├── tsconfig.json +├── .env.example +├── README.md +├── src/ +│ ├── 01-act-cache-with-variables.ts # Demo 1 +│ └── 02-agent-cache-test.ts # Demo 2 +└── .cache/ # Cache directory (created on run) + ├── act-cache/ # Act cache files + └── agent-cache/ # Agent cache files +``` + +## Commands + +```bash +# Run demos +npm run demo:act-cache # Demo 1: Act with variables +npm run demo:agent-cache # Demo 2: Agent caching +npm run demo:all # Both demos + +# Cache management +npm run clear-cache # Clear all cache files + +# Run with fresh cache +npm run demo:act-cache -- --fresh +npm run demo:agent-cache -- --fresh + +# Inspect cache contents +cat .cache/act-cache/*.json | jq . +cat .cache/agent-cache/*.json | jq . +``` + +## Environment Variables + +| Variable | Required | Description | +| ------------------------ | -------- | ------------------------------------- | +| `OPENAI_API_KEY` | Yes | OpenAI API key for LLM calls | +| `ANTHROPIC_API_KEY` | No | Anthropic API key (for Claude models) | +| `BROWSERBASE_API_KEY` | No | For running in Browserbase cloud | +| `BROWSERBASE_PROJECT_ID` | No | For running in Browserbase cloud | + +## Troubleshooting + +### Cache not being hit + +1. Make sure you're using the same instruction text +2. Check that the URL matches (exact match required) +3. Verify variable keys are the same (order matters) + +### LLM errors + +1. Check your `OPENAI_API_KEY` is set correctly +2. Ensure you have API credits available +3. Try a different model if rate limited + +### Browser not launching + +1. Make sure Playwright is installed: `npx playwright install chromium` +2. Check you're running in LOCAL env (not BROWSERBASE without credentials) diff --git a/packages/examples/demos/caching-with-variables/package.json b/packages/examples/demos/caching-with-variables/package.json new file mode 100644 index 0000000000..b60a5b5205 --- /dev/null +++ b/packages/examples/demos/caching-with-variables/package.json @@ -0,0 +1,24 @@ +{ + "name": "stagehand-caching-demo", + "version": "1.0.0", + "description": "Demo showcasing Stagehand caching with variables for privacy-preserving performance optimization", + "type": "module", + "scripts": { + "demo": "tsx src/act-cache-demo.ts", + "demo:act-cache": "tsx src/01-act-cache-with-variables.ts", + "demo:agent-cache": "tsx src/02-agent-cache-test.ts", + "demo:all": "npm run demo:act-cache && npm run demo:agent-cache", + "clear-cache": "rm -rf .cache && echo 'Cache cleared!'" + }, + "dependencies": { + "@browserbasehq/sdk": "latest", + "@browserbasehq/stagehand": "3.0.7", + "dotenv": "^16.4.5", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/packages/examples/demos/caching-with-variables/src/01-act-cache-with-variables.ts b/packages/examples/demos/caching-with-variables/src/01-act-cache-with-variables.ts new file mode 100644 index 0000000000..85da52ab10 --- /dev/null +++ b/packages/examples/demos/caching-with-variables/src/01-act-cache-with-variables.ts @@ -0,0 +1,268 @@ +/** + * Demo 1: Stagehand Act Caching with Variables + * + * This demo demonstrates how Stagehand's caching works with variables, + * specifically showing that: + * + * 1. Variable VALUES are NOT stored in the cache (privacy preservation) + * 2. Only variable KEYS are stored in the cache + * 3. Cache still works effectively with different variable values + * 4. Significant performance improvement on subsequent runs + * + * How the cache key works: + * - Cache key = hash(instruction + URL + variableKeys) + * - Variable values are NOT part of the cache key + * - At replay time, current variable values are substituted + * + * Run this demo multiple times with different usernames to see: + * - First run: LLM inference (slow, ~2-3s per action) + * - Subsequent runs: Cache hit (fast, <100ms per action) + * - Cache file does NOT contain the actual username + */ + +import { Stagehand } from "@browserbasehq/stagehand"; +import * as fs from "fs"; +import * as path from "path"; +import "dotenv/config"; + +const CACHE_DIR = path.join(process.cwd(), ".cache", "act-cache"); + +// Different usernames to test with - demonstrates cache works with different values +const TEST_USERNAMES = [ + "john.doe@example.com", + "jane.smith@example.com", + "secret.user@example.com", +]; + +interface RunResult { + username: string; + elapsed: number; + cacheHit: boolean; +} + +async function runWithVariables(username: string, runNumber: number): Promise { + console.log(`\n${"=".repeat(60)}`); + console.log(`RUN ${runNumber}: Testing with username: ${username}`); + console.log("=".repeat(60)); + + const startTime = Date.now(); + + const stagehand = new Stagehand({ + env: "LOCAL", + verbose: 1, + model: "gpt-4o-mini", + cacheDir: CACHE_DIR, + }); + + await stagehand.init(); + const page = stagehand.context.pages()[0]; + + try { + // Using a simple form page for demonstration + console.log("\nNavigating to demo form..."); + await page.goto("https://the-internet.herokuapp.com/login"); + await page.waitForLoadState("domcontentloaded"); + + // Check if cache exists before this run + const cacheExistedBefore = fs.existsSync(CACHE_DIR) && fs.readdirSync(CACHE_DIR).length > 0; + + console.log(`\nCache before run: ${cacheExistedBefore ? "EXISTS" : "EMPTY"}`); + + // Use act() with variables - the %username% placeholder will be replaced + // with the actual value at runtime, but NOT stored in the cache + console.log("\nExecuting act() with variable..."); + console.log(` Instruction: Type %username% into the username field`); + console.log(` Variable: username = "${username}"`); + + const actStartTime = Date.now(); + + await stagehand.act("Type %username% into the username field", { + variables: { username }, + }); + + const actElapsed = Date.now() - actStartTime; + + // Determine if this was a cache hit based on timing + // LLM calls typically take 1-3 seconds, cache hits are <200ms + const cacheHit = actElapsed < 500; + + console.log(`\nAction completed in ${actElapsed}ms`); + console.log( + `Cache ${cacheHit ? "HIT" : "MISS"} (${cacheHit ? "instant replay" : "LLM inference"})`, + ); + + const elapsed = Date.now() - startTime; + + await stagehand.close(); + + return { + username, + elapsed, + cacheHit, + }; + } catch (error) { + console.error("Error:", error); + await stagehand.close(); + throw error; + } +} + +async function inspectCacheContents() { + console.log(`\n${"=".repeat(60)}`); + console.log("CACHE INSPECTION"); + console.log("=".repeat(60)); + + if (!fs.existsSync(CACHE_DIR)) { + console.log("\nNo cache directory found."); + return; + } + + const cacheFiles = fs.readdirSync(CACHE_DIR).filter((f) => f.endsWith(".json")); + console.log(`\nFound ${cacheFiles.length} cache file(s):`); + + for (const file of cacheFiles) { + const filePath = path.join(CACHE_DIR, file); + const content = JSON.parse(fs.readFileSync(filePath, "utf-8")); + + console.log(`\n--- ${file} ---`); + console.log(` Version: ${content.version}`); + console.log(` Instruction: "${content.instruction}"`); + console.log(` URL: ${content.url}`); + console.log(` Variable Keys: ${JSON.stringify(content.variableKeys)}`); + console.log(` Actions: ${content.actions?.length ?? 0} action(s)`); + + // Show action details + if (content.actions && content.actions.length > 0) { + for (const action of content.actions) { + console.log(` - Method: ${action.method}`); + console.log(` Arguments: ${JSON.stringify(action.arguments)}`); + console.log(` Selector: ${action.selector?.substring(0, 50)}...`); + } + } + + // IMPORTANT: Check if username value is in the cache + const cacheString = JSON.stringify(content); + const containsSecretUser = TEST_USERNAMES.some((u) => cacheString.includes(u)); + + console.log(`\n PRIVACY CHECK:`); + console.log( + ` Contains any test username value? ${containsSecretUser ? "YES (BAD!)" : "NO (GOOD!)"}`, + ); + console.log( + ` Variable placeholder preserved? ${cacheString.includes("%username%") ? "YES" : "NO"}`, + ); + } +} + +async function main() { + console.log(` +${"#".repeat(60)} +# Stagehand Act Caching with Variables Demo +# +# Demonstrates privacy-preserving cache mechanism: +# - Variable VALUES are NOT stored in cache +# - Only variable KEYS are stored +# - Cache works with different values +${"#".repeat(60)} +`); + + // Clear cache for fresh demo + const args = process.argv.slice(2); + if (args.includes("--fresh")) { + console.log("Clearing cache for fresh run..."); + if (fs.existsSync(CACHE_DIR)) { + fs.rmSync(CACHE_DIR, { recursive: true }); + } + } + + const results: RunResult[] = []; + + // Run 1: First username - should be cache MISS (LLM inference) + console.log("\n\n>>> PHASE 1: First run with first username (expect cache MISS)"); + results.push(await runWithVariables(TEST_USERNAMES[0], 1)); + + // Run 2: Same username again - should be cache HIT + console.log("\n\n>>> PHASE 2: Second run with same username (expect cache HIT)"); + results.push(await runWithVariables(TEST_USERNAMES[0], 2)); + + // Run 3: DIFFERENT username - should STILL be cache HIT! + // This proves that the cache key is based on variable KEYS, not VALUES + console.log("\n\n>>> PHASE 3: Run with DIFFERENT username (should STILL be cache HIT!)"); + console.log(">>> This proves variable VALUES are not part of the cache key"); + results.push(await runWithVariables(TEST_USERNAMES[1], 3)); + + // Run 4: Another different username + console.log("\n\n>>> PHASE 4: Run with yet another username (should be cache HIT)"); + results.push(await runWithVariables(TEST_USERNAMES[2], 4)); + + // Inspect cache contents + await inspectCacheContents(); + + // Summary + console.log(`\n${"=".repeat(60)}`); + console.log("RESULTS SUMMARY"); + console.log("=".repeat(60)); + + console.log("\n| Run | Username | Time (ms) | Cache |"); + console.log("|-----|-----------------------------|-----------| -------|"); + + for (let i = 0; i < results.length; i++) { + const r = results[i]; + const usernameDisplay = r.username.substring(0, 25).padEnd(27); + const timeDisplay = r.elapsed.toString().padStart(9); + const cacheDisplay = r.cacheHit ? "HIT " : "MISS"; + console.log(`| ${i + 1} | ${usernameDisplay} | ${timeDisplay} | ${cacheDisplay} |`); + } + + console.log(`\n${"=".repeat(60)}`); + console.log("KEY FINDINGS"); + console.log("=".repeat(60)); + + console.log(` +1. PRIVACY PRESERVED: + - Cache file does NOT contain actual username values + - Only the variable KEY "username" is stored + - Variable placeholder %username% is preserved in cache + +2. CACHE EFFICIENCY: + - First run: Cache miss (LLM inference required) + - All subsequent runs: Cache hit (instant replay) + - Different usernames still hit the same cache! + +3. HOW IT WORKS: + - Cache key = hash(instruction + URL + variableKeys) + - Variable VALUES are not part of the cache key + - At replay, current values replace %placeholder% tokens + +4. PERFORMANCE BENEFIT: + - Cache hit: <100ms per action + - Cache miss: 1-3 seconds per action (LLM call) + - ${results.length > 1 ? `Speedup: ~${Math.round(results[0].elapsed / results[1].elapsed)}x faster with cache` : ""} + +5. USE CASES: + - Login forms with different users + - Payment forms with different card numbers + - Search inputs with different queries + - Any action with sensitive/variable data +`); + + console.log(`\n${"=".repeat(60)}`); + console.log("TRY IT YOURSELF"); + console.log("=".repeat(60)); + + console.log(` +1. Run again (cache will be hit): + npm run demo:act-cache + +2. Run with fresh cache: + npm run demo:act-cache -- --fresh + +3. Clear cache manually: + npm run clear-cache + +4. Inspect cache contents: + cat .cache/act-cache/*.json | jq . +`); +} + +main().catch(console.error); diff --git a/packages/examples/demos/caching-with-variables/src/02-agent-cache-test.ts b/packages/examples/demos/caching-with-variables/src/02-agent-cache-test.ts new file mode 100644 index 0000000000..00eab96e2a --- /dev/null +++ b/packages/examples/demos/caching-with-variables/src/02-agent-cache-test.ts @@ -0,0 +1,280 @@ +/** + * Demo 2: Stagehand Agent Caching Test + * + * This demo tests whether Stagehand's agent() API uses caching. + * + * Key findings from code analysis: + * - YES, Agent does have caching support (AgentCache.ts) + * - Agent cache stores: instruction, startUrl, options, configSignature, steps, result + * - Cache key = hash(instruction + startUrl + options + configSignature) + * - Agent does NOT currently support variables like act() does + * + * This demo: + * 1. Tests agent caching with a simple task + * 2. Runs the same task twice to observe cache behavior + * 3. Compares performance between first and second runs + * + * Future: Test agent with variables (if/when supported) + */ + +import { Stagehand } from "@browserbasehq/stagehand"; +import * as fs from "fs"; +import * as path from "path"; +import "dotenv/config"; + +const CACHE_DIR = path.join(process.cwd(), ".cache", "agent-cache"); + +interface RunResult { + runNumber: number; + elapsed: number; + stepCount: number; + success: boolean; + cacheHit: boolean; +} + +async function runAgentTask(runNumber: number): Promise { + console.log(`\n${"=".repeat(60)}`); + console.log(`AGENT RUN ${runNumber}`); + console.log("=".repeat(60)); + + const startTime = Date.now(); + + const stagehand = new Stagehand({ + env: "LOCAL", + verbose: 1, + model: "gpt-4o-mini", + cacheDir: CACHE_DIR, + }); + + await stagehand.init(); + const page = stagehand.context.pages()[0]; + + try { + // Check if cache exists before this run + const cacheExistedBefore = + fs.existsSync(CACHE_DIR) && + fs.readdirSync(CACHE_DIR).filter((f) => f.startsWith("agent-")).length > 0; + + console.log(`\nCache before run: ${cacheExistedBefore ? "EXISTS" : "EMPTY"}`); + + // Navigate to a simple page + console.log("\nNavigating to demo page..."); + await page.goto("https://the-internet.herokuapp.com/"); + await page.waitForLoadState("domcontentloaded"); + + // Create agent and execute a simple task + console.log("\nCreating agent..."); + const agent = stagehand.agent({ + model: "gpt-4o", // Agent requires a capable model + }); + + console.log("\nExecuting agent task..."); + console.log(` Instruction: "Click on the 'Form Authentication' link"`); + + const agentStartTime = Date.now(); + + const result = await agent.execute({ + instruction: "Click on the 'Form Authentication' link", + maxSteps: 5, + }); + + const agentElapsed = Date.now() - agentStartTime; + + // Determine if this was a cache hit based on timing and metadata + const cacheHit = (result as any).metadata?.cacheHit === true || agentElapsed < 1000; + + console.log(`\nAgent completed in ${agentElapsed}ms`); + console.log(`Success: ${result.success}`); + console.log(`Message: ${result.message}`); + console.log(`Steps: ${result.actions?.length ?? 0}`); + console.log(`Cache: ${cacheHit ? "HIT" : "MISS"}`); + + if ((result as any).metadata?.cacheHit) { + console.log(`Cache Timestamp: ${(result as any).metadata?.cacheTimestamp}`); + } + + const elapsed = Date.now() - startTime; + + await stagehand.close(); + + return { + runNumber, + elapsed, + stepCount: result.actions?.length ?? 0, + success: result.success, + cacheHit, + }; + } catch (error) { + console.error("Error:", error); + await stagehand.close(); + throw error; + } +} + +async function inspectAgentCache() { + console.log(`\n${"=".repeat(60)}`); + console.log("AGENT CACHE INSPECTION"); + console.log("=".repeat(60)); + + if (!fs.existsSync(CACHE_DIR)) { + console.log("\nNo cache directory found."); + return; + } + + const cacheFiles = fs.readdirSync(CACHE_DIR).filter((f) => f.startsWith("agent-")); + console.log(`\nFound ${cacheFiles.length} agent cache file(s):`); + + for (const file of cacheFiles) { + const filePath = path.join(CACHE_DIR, file); + const content = JSON.parse(fs.readFileSync(filePath, "utf-8")); + + console.log(`\n--- ${file} ---`); + console.log(` Version: ${content.version}`); + console.log(` Instruction: "${content.instruction}"`); + console.log(` Start URL: ${content.startUrl}`); + console.log(` Options: ${JSON.stringify(content.options)}`); + console.log(` Steps: ${content.steps?.length ?? 0} step(s)`); + console.log(` Timestamp: ${content.timestamp}`); + + // Show step types + if (content.steps && content.steps.length > 0) { + console.log(` Step Types:`); + for (const step of content.steps) { + console.log(` - ${step.type}${step.instruction ? `: "${step.instruction}"` : ""}`); + } + } + + // Show result summary + if (content.result) { + console.log(` Result:`); + console.log(` Success: ${content.result.success}`); + console.log(` Message: ${content.result.message}`); + console.log(` Actions: ${content.result.actions?.length ?? 0}`); + } + } +} + +async function main() { + console.log(` +${"#".repeat(60)} +# Stagehand Agent Caching Test Demo +# +# Tests whether agent() uses caching: +# - Run 1: Expected cache MISS (first execution) +# - Run 2: Expected cache HIT (replay from cache) +${"#".repeat(60)} +`); + + // Clear cache for fresh demo + const args = process.argv.slice(2); + if (args.includes("--fresh")) { + console.log("Clearing cache for fresh run..."); + if (fs.existsSync(CACHE_DIR)) { + fs.rmSync(CACHE_DIR, { recursive: true }); + } + } + + const results: RunResult[] = []; + + // Run 1: First execution - should be cache MISS + console.log("\n\n>>> PHASE 1: First agent run (expect cache MISS)"); + results.push(await runAgentTask(1)); + + // Wait a moment between runs + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Run 2: Same task again - should be cache HIT + console.log("\n\n>>> PHASE 2: Second agent run (expect cache HIT)"); + results.push(await runAgentTask(2)); + + // Inspect cache contents + await inspectAgentCache(); + + // Summary + console.log(`\n${"=".repeat(60)}`); + console.log("RESULTS SUMMARY"); + console.log("=".repeat(60)); + + console.log("\n| Run | Time (ms) | Steps | Success | Cache |"); + console.log("|-----|-----------|-------|---------|--------|"); + + for (const r of results) { + const timeDisplay = r.elapsed.toString().padStart(9); + const stepsDisplay = r.stepCount.toString().padStart(5); + const successDisplay = r.success ? "Yes" : "No "; + const cacheDisplay = r.cacheHit ? "HIT " : "MISS"; + console.log( + `| ${r.runNumber} | ${timeDisplay} | ${stepsDisplay} | ${successDisplay} | ${cacheDisplay} |`, + ); + } + + if (results.length >= 2) { + const speedup = (results[0].elapsed / results[1].elapsed).toFixed(1); + console.log(`\nPerformance improvement: ${speedup}x faster with cache`); + } + + console.log(`\n${"=".repeat(60)}`); + console.log("KEY FINDINGS"); + console.log("=".repeat(60)); + + const agentCacheExists = + fs.existsSync(CACHE_DIR) && + fs.readdirSync(CACHE_DIR).filter((f) => f.startsWith("agent-")).length > 0; + + console.log(` +1. AGENT CACHING STATUS: + - Cache files created: ${agentCacheExists ? "YES" : "NO"} + - Second run faster: ${results.length >= 2 ? (results[1].elapsed < results[0].elapsed ? "YES" : "NO") : "N/A"} + +2. HOW AGENT CACHE WORKS: + - Cache key = hash(instruction + startUrl + options + configSignature) + - Stores: steps (act, goto, scroll, etc.) + final result + - On replay: executes cached steps without LLM calls + +3. CURRENT LIMITATIONS: + - Agent does NOT support variables like act() does + - Cannot use %placeholder% syntax in agent instructions + - Variable data in agent instructions is stored in cache + +4. FUTURE CONSIDERATION: + - Agent with variables would need similar mechanism to act() + - Would need to strip variable values from cached steps + - Would need to inject values during replay +`); + + console.log(`\n${"=".repeat(60)}`); + console.log("COMPARISON: ACT vs AGENT CACHING"); + console.log("=".repeat(60)); + + console.log(` +| Feature | act() Cache | agent() Cache | +|----------------------------|-------------|---------------| +| Caching supported | Yes | Yes | +| Variables supported | Yes | No | +| Privacy preservation | Yes | No | +| Cache key based on | instr+url+ | instr+url+ | +| | varKeys | options+config| +| Replays actions | Yes | Yes | +| Multi-step workflows | Single act | Full workflow | +`); + + console.log(`\n${"=".repeat(60)}`); + console.log("TRY IT YOURSELF"); + console.log("=".repeat(60)); + + console.log(` +1. Run again (cache will be hit): + npm run demo:agent-cache + +2. Run with fresh cache: + npm run demo:agent-cache -- --fresh + +3. Clear cache manually: + npm run clear-cache + +4. Inspect cache contents: + cat .cache/agent-cache/agent-*.json | jq . +`); +} + +main().catch(console.error); diff --git a/packages/examples/demos/caching-with-variables/src/act-cache-demo.ts b/packages/examples/demos/caching-with-variables/src/act-cache-demo.ts new file mode 100644 index 0000000000..c9f0615399 --- /dev/null +++ b/packages/examples/demos/caching-with-variables/src/act-cache-demo.ts @@ -0,0 +1,51 @@ +/** + * Stagehand Act Caching with Variables + * + * Demonstrates privacy-preserving caching: + * - Variable VALUES are NOT stored in cache (only keys) + * - Different values still hit the same cache entry + * - ~100x speedup on cache hits + */ + +import { Stagehand } from "@browserbasehq/stagehand"; +import "dotenv/config"; + +const CACHE_DIR = ".cache/act-cache"; + +async function demo() { + const usernames = ["john.doe@example.com", "jane.smith@example.com", "secret.user@example.com"]; + + for (let i = 0; i < usernames.length; i++) { + const username = usernames[i]; + console.log(`\nRun ${i + 1}: ${username}`); + + const stagehand = new Stagehand({ + env: "LOCAL", + verbose: 0, + cacheDir: CACHE_DIR, + }); + + await stagehand.init(); + const page = stagehand.context.pages()[0]; + + await page.goto("https://the-internet.herokuapp.com/login"); + await page.waitForLoadState("domcontentloaded"); + + const start = Date.now(); + + // The %username% placeholder is replaced at runtime + // but NOT stored in cache - only the key "username" is cached + await stagehand.act("Type %username% into the username field", { + variables: { username }, + }); + + const elapsed = Date.now() - start; + const cacheHit = elapsed < 500; + + console.log(` → ${elapsed}ms (${cacheHit ? "CACHE HIT" : "CACHE MISS"})`); + + await stagehand.close(); + } +} + +demo().catch(console.error); diff --git a/packages/examples/demos/caching-with-variables/tsconfig.json b/packages/examples/demos/caching-with-variables/tsconfig.json new file mode 100644 index 0000000000..65e0cfaa78 --- /dev/null +++ b/packages/examples/demos/caching-with-variables/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist", + "declaration": true, + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/examples/demos/company-news-function/.browserbase/functions/manifests/company-news-finder.json b/packages/examples/demos/company-news-function/.browserbase/functions/manifests/company-news-finder.json new file mode 100644 index 0000000000..7e7921d597 --- /dev/null +++ b/packages/examples/demos/company-news-function/.browserbase/functions/manifests/company-news-finder.json @@ -0,0 +1,11 @@ +{ + "name": "company-news-finder", + "config": { + "sessionConfig": { + "browserSettings": { + "advancedStealth": true + } + }, + "parametersSchema": {} + } +} diff --git a/packages/examples/demos/company-news-function/.env.example b/packages/examples/demos/company-news-function/.env.example new file mode 100644 index 0000000000..9ec14a6f2d --- /dev/null +++ b/packages/examples/demos/company-news-function/.env.example @@ -0,0 +1,10 @@ +# Browserbase Configuration +BROWSERBASE_API_KEY= +BROWSERBASE_PROJECT_ID= + +# AI Model API Key (Required for Stagehand) +# Get your key from one of these providers: +# - Google Gemini: https://aistudio.google.com/apikey +# - OpenAI: https://platform.openai.com/api-keys +# - Anthropic: https://console.anthropic.com/ +MODEL_API_KEY= diff --git a/packages/examples/demos/company-news-function/.gitignore b/packages/examples/demos/company-news-function/.gitignore new file mode 100644 index 0000000000..3e7467a42e --- /dev/null +++ b/packages/examples/demos/company-news-function/.gitignore @@ -0,0 +1,24 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.*.local + +# Build outputs +dist/ +build/ + +# Logs +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# Claude +.claude/ diff --git a/packages/examples/demos/company-news-function/README.md b/packages/examples/demos/company-news-function/README.md new file mode 100644 index 0000000000..f8fd8034df --- /dev/null +++ b/packages/examples/demos/company-news-function/README.md @@ -0,0 +1,371 @@ +# Company News Finder Function + +A Browserbase Function that uses Stagehand's AI agent to search Google for the latest news about a company and provide an intelligent summary along with top news links. + +## What It Does + +This function takes a company name, searches Google for the latest news about that company, and uses Stagehand's AI-powered agent to analyze the results. It returns: + +- **Summary** - A comprehensive 2-3 paragraph summary of what's currently happening with the company +- **Top News Links** - The most relevant and recent news articles with titles, URLs, and sources +- **Metadata** - Execution time, session replay URL, and timestamp + +**Use Cases:** + +- Get quick updates on what's happening with a competitor +- Research a company before a meeting or interview +- Track news about companies in your portfolio +- Monitor industry leaders and their recent developments +- Stay informed about customer or partner companies + +## Prerequisites + +1. **Browserbase Account** - Sign up at [browserbase.com](https://www.browserbase.com) +2. **API Key** - Get your API key from [Settings](https://www.browserbase.com/settings) +3. **AI Model API Key** - Get an API key from your AI provider (see below) +4. **Node.js** - Version 18 or higher + +## Setup + +1. **Install dependencies:** + +```bash +npm install +``` + +2. **Set environment variables:** + +```bash +export BROWSERBASE_API_KEY="your-api-key-here" +export BROWSERBASE_PROJECT_ID="your-project-id-here" +export MODEL_API_KEY="your-ai-model-api-key-here" +``` + +Or create a `.env` file: + +```env +BROWSERBASE_API_KEY=your-api-key-here +BROWSERBASE_PROJECT_ID=your-project-id-here +MODEL_API_KEY=your-ai-model-api-key-here +``` + +**Note:** The `MODEL_API_KEY` is required for Stagehand's AI agent. Get an API key from your AI provider: + +- **Google Gemini** (recommended): [Google AI Studio](https://aistudio.google.com/apikey) +- **OpenAI**: [OpenAI Platform](https://platform.openai.com/api-keys) +- **Anthropic**: [Anthropic Console](https://console.anthropic.com/) + +## Deploy the Function + +Deploy the function to Browserbase: + +```bash +npm run deploy +``` + +This will: + +1. Bundle the function code +2. Upload it to Browserbase +3. Return a Function ID you can invoke + +**Save the Function ID** from the output - you'll need it to invoke the function. + +## Test the Function + +### Option 1: Test via Browserbase Dashboard + +1. Go to [Browserbase Dashboard → Functions](https://www.browserbase.com/functions) +2. Find your `company-news-finder` function +3. Click "Test" or "Invoke" +4. Provide test parameters: + +```json +{ + "companyName": "Tesla", + "model": "google/gemini-3-flash-preview", + "maxSteps": 30 +} +``` + +**Note:** The `model` and `maxSteps` parameters are optional and will use defaults if not provided. + +5. Click "Invoke" and watch the results appear + +### Option 2: Test via API + +Use the Functions API to invoke directly: + +```bash +# Get your Function ID +FUNCTION_ID="func_xxxxxxxxxxxxx" + +# Invoke the function +curl -X POST "https://api.browserbase.com/v1/functions/${FUNCTION_ID}/invoke" \ + -H "x-bb-api-key: ${BROWSERBASE_API_KEY}" \ + -H "Content-Type: application/json" \ + -d '{ + "params": { + "companyName": "OpenAI", + "model": "google/gemini-3-flash-preview" + } + }' +``` + +This returns an invocation ID. Poll for results: + +```bash +INVOCATION_ID="inv_xxxxxxxxxxxxx" + +curl "https://api.browserbase.com/v1/functions/invocations/${INVOCATION_ID}" \ + -H "x-bb-api-key: ${BROWSERBASE_API_KEY}" +``` + +### Option 3: Test with Node.js Script + +Create `test.js`: + +```javascript +const BROWSERBASE_API_KEY = process.env.BROWSERBASE_API_KEY; +const FUNCTION_ID = "func_xxxxxxxxxxxxx"; // Your function ID + +async function testCompanyNewsFinder() { + // Invoke function + const invokeRes = await fetch(`https://api.browserbase.com/v1/functions/${FUNCTION_ID}/invoke`, { + method: "POST", + headers: { + "x-bb-api-key": BROWSERBASE_API_KEY, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + params: { + companyName: "Microsoft", + model: "google/gemini-3-flash-preview", + }, + }), + }); + + const { id: invocationId } = await invokeRes.json(); + console.log("Invocation ID:", invocationId); + + // Poll for completion + let status = "RUNNING"; + let result; + + while (status === "RUNNING") { + await new Promise((resolve) => setTimeout(resolve, 3000)); + + const pollRes = await fetch( + `https://api.browserbase.com/v1/functions/invocations/${invocationId}`, + { + headers: { "x-bb-api-key": BROWSERBASE_API_KEY }, + }, + ); + + result = await pollRes.json(); + status = result.status; + console.log("Status:", status); + } + + console.log("\n=== Company News Summary ==="); + console.log(result.results.summary); + console.log("\n=== Top News Links ==="); + result.results.topLinks.forEach((link, i) => { + console.log(`\n${i + 1}. ${link.title}`); + console.log(` Source: ${link.source || "Unknown"}`); + console.log(` URL: ${link.url}`); + }); + console.log(`\nSession Replay: ${result.results.metadata.sessionReplayUrl}`); +} + +testCompanyNewsFinder(); +``` + +Run it: + +```bash +node test.js +``` + +## Example Output + +```json +{ + "success": true, + "companyName": "Tesla", + "summary": "Tesla has been making headlines recently with several major developments. The company reported strong Q4 earnings, beating analyst expectations with record vehicle deliveries despite ongoing supply chain challenges. CEO Elon Musk announced plans for a new manufacturing facility in Southeast Asia, marking Tesla's continued global expansion.\n\nIn other news, Tesla's Full Self-Driving (FSD) beta program reached a new milestone with over 500,000 active users. The company also unveiled updated versions of the Model 3 and Model Y with improved range and new features. However, Tesla faces increased competition from traditional automakers and Chinese EV manufacturers who are rapidly expanding their electric vehicle offerings.", + "topLinks": [ + { + "title": "Tesla Reports Record Q4 Earnings, Beats Expectations", + "url": "https://www.reuters.com/business/tesla-earnings-q4-2024", + "source": "Reuters" + }, + { + "title": "Elon Musk Announces New Tesla Factory in Southeast Asia", + "url": "https://www.bloomberg.com/news/tesla-factory-asia", + "source": "Bloomberg" + }, + { + "title": "Tesla's FSD Beta Reaches 500,000 Active Users", + "url": "https://techcrunch.com/tesla-fsd-milestone", + "source": "TechCrunch" + }, + { + "title": "Updated Model 3 and Model Y Feature Improved Range", + "url": "https://www.theverge.com/tesla-model-update", + "source": "The Verge" + }, + { + "title": "Tesla Faces Growing Competition from Chinese EV Makers", + "url": "https://www.cnbc.com/tesla-competition-china", + "source": "CNBC" + } + ], + "metadata": { + "totalLinks": 5, + "scrapedAt": "2024-01-15T10:30:00.000Z", + "duration": 25834, + "sessionReplayUrl": "https://www.browserbase.com/sessions/sess_xxxxx" + } +} +``` + +## How It Works + +1. **Connect** - Function connects to Browserbase browser session via CDP +2. **Navigate to Google** - Browser visits google.com +3. **Search** - AI agent types the company name + "latest news" and submits search +4. **Analyze Results** - Agent reads headlines, snippets, and sources from top results +5. **Generate Summary** - Agent creates a comprehensive summary of current developments +6. **Extract Links** - Agent collects the top 5-7 most relevant news articles +7. **Return Data** - Returns clean JSON with summary, links, and metadata +8. **Session Replay** - Full browser session recorded for debugging + +**Technical Details:** + +- Uses Playwright's Chrome DevTools Protocol (CDP) connection +- Stagehand runs in "LOCAL" mode using the existing browser session +- Agent operates in "hybrid" mode for optimal performance +- Default model: `google/gemini-3-flash-preview` (configurable) +- Searches Google and analyzes results in real-time + +**Key Features:** + +- ✅ **AI-Powered Summary** - Get the big picture quickly +- ✅ **Latest News** - Always searches for recent developments +- ✅ **Top Sources** - Links to original articles from major news outlets +- ✅ **Fast** - Typically completes in 20-30 seconds +- ✅ **Debuggable** - Session replay for every run +- ✅ **Flexible** - Works for any company name +- ✅ **Configurable** - Choose your preferred AI model + +## Troubleshooting + +### Function times out + +- Google search results load slowly sometimes +- Check the session replay URL to see where it got stuck +- Try increasing `maxSteps` if the agent needs more time + +### Summary is empty or links missing + +- The agent may not have found clear news results +- Try a more specific company name (e.g., "Tesla Inc" instead of just "Tesla") +- Check the session replay to see what Google returned +- Increase `maxSteps` to give the agent more time + +### API Key errors + +- Verify your `BROWSERBASE_API_KEY` is set correctly +- Ensure your `MODEL_API_KEY` is set and valid for your chosen AI provider +- Ensure your account has Functions enabled +- Check that you're using the correct project ID + +### Deployment fails + +- Make sure you have the latest `@browserbasehq/sdk-functions` package +- Verify your TypeScript configuration is correct +- Check that all dependencies are installed + +### Model errors + +- Verify your AI model API key is correct for the provider +- Check that the model name is valid (e.g., `google/gemini-3-flash-preview`) +- Ensure you have sufficient credits with your AI provider + +### Google blocks the search + +- Browserbase uses advanced stealth mode to avoid detection +- If blocked, check the session replay to see what happened +- This is rare but can happen with high request volumes + +## Customization + +### Adjust the search query + +Edit the instruction in `index.ts` to modify the search: + +```typescript +instruction: `Search Google for "${params.companyName} news 2024" and analyze the results. + Focus on financial news and company announcements. + Exclude opinion pieces and analysis articles.`; +``` + +### Change the AI model + +You can use different AI models by passing the `model` parameter: + +```json +{ + "companyName": "Apple", + "model": "openai/gpt-4o" +} +``` + +Supported models: + +- `google/gemini-3-flash-preview` (default, fast and cost-effective) +- `openai/gpt-4o` (powerful, requires OpenAI API key) +- `anthropic/claude-3.5-sonnet` (excellent reasoning, requires Anthropic API key) + +### Adjust agent steps + +Control how many steps the agent takes: + +```json +{ + "companyName": "Amazon", + "maxSteps": 40 +} +``` + +Higher `maxSteps` allows more thorough analysis but takes longer. + +### Modify summary style + +Edit the instruction to change the summary format: + +```typescript +instruction: `Search Google for "${params.companyName} latest news" and analyze the results. + + Create a summary that: + - Starts with the most important recent development + - Focuses on financial and business metrics + - Is written in a professional, objective tone + - Includes specific dates and numbers when available + + Return JSON with "summary" and "topLinks" fields.`; +``` + +## Resources + +- [Browserbase Functions Documentation](https://docs.browserbase.com/features/functions) +- [Stagehand Documentation](https://docs.browserbase.com/guides/stagehand) +- [Functions API Reference](https://docs.browserbase.com/functions/reference) +- [Session Replay](https://docs.browserbase.com/features/session-replay) + +## Support + +- [Discord Community](https://discord.gg/browserbase) +- [GitHub Issues](https://github.com/browserbase/sdk-functions/issues) +- [Email Support](mailto:support@browserbase.com) diff --git a/packages/examples/demos/company-news-function/index.ts b/packages/examples/demos/company-news-function/index.ts new file mode 100644 index 0000000000..d14d454375 --- /dev/null +++ b/packages/examples/demos/company-news-function/index.ts @@ -0,0 +1,116 @@ +import { defineFn } from "@browserbasehq/sdk-functions"; +import { chromium } from "playwright-core"; +import { Stagehand } from "@browserbasehq/stagehand"; +import { z } from "zod"; + +const parametersSchema = z.object({ + companyName: z.string().describe("The company name to search for news about"), + apiKey: z.string().describe("The AI model API key"), + model: z + .string() + .optional() + .describe("The AI model to use (default: anthropic/claude-sonnet-4-20250514)"), + maxSteps: z.number().optional().describe("Maximum steps for agent execution (default: 30)"), +}); + +defineFn( + "company-news-finder", + async (context, params) => { + const { session } = context; + const startTime = Date.now(); + + try { + console.log("Connecting to browser session:", session.id); + console.log(`Searching for latest news about: ${params.companyName}`); + + // Connect to the browser instance + const browser = await chromium.connectOverCDP(session.connectUrl); + const browserContext = browser.contexts()[0]!; + const page = browserContext.pages()[0]!; + + console.log("Navigating to Google..."); + await page.goto("https://www.google.com", { waitUntil: "domcontentloaded" }); + + // Wait a moment for the page to fully load + await page.waitForTimeout(1000); + + // Configure Stagehand to use the existing browser session + const stagehand = new Stagehand({ + model: { + modelName: params.model ?? "anthropic/claude-sonnet-4-20250514", + apiKey: params.apiKey, + }, + env: "LOCAL", + localBrowserLaunchOptions: { + cdpUrl: session.connectUrl, + }, + experimental: true, + }); + + await stagehand.init(); + + console.log("Stagehand initialized, searching for company news..."); + + // Use Stagehand agent to search Google and analyze news + const agent = stagehand.agent({ + mode: "hybrid", + model: params.model ?? "anthropic/claude-sonnet-4-20250514", + systemPrompt: `You are a helpful assistant that searches for company news and provides summaries.`, + }); + + const result = await agent.execute({ + instruction: `Search Google for "${params.companyName} latest news" and analyze the results. + + Steps: + 1. In the Google search box, type "${params.companyName} latest news" and submit the search + 2. Wait for the results to load + 3. Look at the top news articles (typically the first 5-10 results) + 4. Read the headlines, snippets, and sources + 5. Create a comprehensive summary of what's happening with ${params.companyName} based on the news headlines and snippets + + Return a JSON object with: + - "summary": A 2-3 paragraph summary of the current situation and recent news about ${params.companyName} + - "topLinks": An array of the top 5-7 news articles with "title", "url", and "source" fields + + Make the summary informative and capture the key themes and developments.`, + maxSteps: params.maxSteps ?? 30, + }); + + console.log("Agent execution completed"); + + // Strip screenshots/large data from actions to stay under 64KB result limit + const agentResult = result as any; + const actions = (agentResult?.actions ?? []).map((a: any) => { + const { screenshot, ...rest } = a; + return rest; + }); + + return { + companyName: params.companyName, + success: agentResult?.success ?? false, + completed: agentResult?.completed ?? false, + message: agentResult?.message ?? "", + actions, + sessionReplayUrl: `https://www.browserbase.com/sessions/${session.id}`, + duration: Date.now() - startTime, + }; + } catch (error) { + console.error("Company news finder failed:", error); + + return { + companyName: params.companyName, + error: error instanceof Error ? error.message : String(error), + sessionReplayUrl: `https://www.browserbase.com/sessions/${session.id}`, + duration: Date.now() - startTime, + }; + } + }, + { + parametersSchema, + sessionConfig: { + browserSettings: { + advancedStealth: true, + }, + }, + }, +); diff --git a/packages/examples/demos/company-news-function/package.json b/packages/examples/demos/company-news-function/package.json new file mode 100644 index 0000000000..3822b7bd48 --- /dev/null +++ b/packages/examples/demos/company-news-function/package.json @@ -0,0 +1,19 @@ +{ + "name": "company-news-finder-function", + "version": "1.0.0", + "description": "Browserbase Function that searches Google for company news and provides AI-generated summaries with top news links", + "main": "index.ts", + "scripts": { + "deploy": "npx @browserbasehq/sdk-functions publish index.ts" + }, + "dependencies": { + "@browserbasehq/sdk-functions": "latest", + "@browserbasehq/stagehand": "3.0.8", + "playwright-core": "^1.48.2", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/examples/demos/company-news-function/tsconfig.json b/packages/examples/demos/company-news-function/tsconfig.json new file mode 100644 index 0000000000..f683de62d1 --- /dev/null +++ b/packages/examples/demos/company-news-function/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["index.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/examples/demos/configurable-browser-trial/.env.example b/packages/examples/demos/configurable-browser-trial/.env.example new file mode 100644 index 0000000000..2a1a727e8a --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/.env.example @@ -0,0 +1,10 @@ +# ── Browserbase ───────────────────────────────────────────────────────────── +# Get these from https://www.browserbase.com/settings +BROWSERBASE_API_KEY= +BROWSERBASE_PROJECT_ID= + +# ── Model (for the AI that drives + grades each task) ──────────────────────── +# Use ONE of the following. Anthropic Claude is the default and recommended. +ANTHROPIC_API_KEY= +# OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +# GOOGLE_GENERATIVE_AI_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxx diff --git a/packages/examples/demos/configurable-browser-trial/.gitignore b/packages/examples/demos/configurable-browser-trial/.gitignore new file mode 100644 index 0000000000..b4924b70d1 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +results/ +*.log +.DS_Store diff --git a/packages/examples/demos/configurable-browser-trial/README.md b/packages/examples/demos/configurable-browser-trial/README.md new file mode 100644 index 0000000000..f9a0193db0 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/README.md @@ -0,0 +1,182 @@ +# bbpoc — Browserbase Verified Trial in a Box + +> Turn a one-week "advanced stealth" trial into a one-day, self-serve smoke test. +> Edit **one file**, run **one command**, get a **leadership-ready scorecard**. + +Every Browserbase proof-of-concept looks the same: a customer hands over a list +of their own bot-protected URLs, then someone spends a week wiring up +concurrency, logging, retries, and stealth settings just to find out the success +rate. `bbpoc` is that week, pre-built. You bring the URLs; it runs them at scale +with stealth + residential proxies, classifies every outcome, and emits the exact +scorecard a Browserbase CE would otherwise assemble by hand. + +``` +┌──────────────┐ ┌─────────────────────────┐ ┌──────────────────────┐ +│ trial.yaml │ ──▶ │ bbpoc run │ ──▶ │ scorecard.html │ +│ your URLs │ │ stealth · proxies · │ │ scorecard.md │ +│ + tasks │ │ captcha · concurrency · │ │ results.json │ +│ + targets │ │ retry-on-new-proxy │ │ (session replays) │ +└──────────────┘ └─────────────────────────┘ └──────────────────────┘ +``` + +--- + +## Quick start (talk to it — recommended) + +This repo ships as a **Claude Code skill**. Open the folder in Claude Code (or +Cursor) and just describe what you want to test — Claude writes the config, runs +the trial, and hands you the scorecard. No YAML, no flags to learn. + +```bash +git clone my-trial && cd my-trial +npm ci +cp .env.example .env # fill in your Browserbase + model keys +``` + +Then, in Claude Code: + +``` +/bbpoc test these for us: + portal.acme.com/login — log in and confirm the dashboard loads + acme.com/search — search "widgets" and read the first result + 80% bar, run each 5 times +``` + +Claude generates `trial.yaml`, runs it on real cloud browsers, and points you at +`results/scorecard.html`. The skill lives in `.claude/skills/bbpoc/` — it's +auto-discovered when you open this repo. To use it in any project, copy that +folder into your global `~/.claude/skills/`. + +## Quick start (CLI — power users / CI) + +Prefer flags? The engine underneath is a normal CLI: + +```bash +npx bbpoc init # scaffolds trial.yaml + .env +# edit trial.yaml — your URLs, tasks, targets +npx bbpoc smoke # quick sanity — 1 attempt per site +npx bbpoc run # full trial — N attempts per site, at concurrency +``` + +Open `results/scorecard.html` and send it to your team. + +> **Need Enterprise/Scale access?** Advanced stealth ("Verified") and residential +> proxies are Enterprise-plan features. If a run errors on session creation, ask +> your Browserbase contact to enable them — or run a control with +> `npx bbpoc run --preset baseline` to see the un-stealthed baseline. + +--- + +## The one file you edit: `trial.yaml` + +```yaml +name: "Acme — Browserbase Verified Trial" +customer: "Acme Inc." + +defaults: + attempts: 3 # run EACH site N times (stability is the real bar) + concurrency: 5 + region: us-west-2 + target: 0.8 # per-site success target + model: anthropic/claude-sonnet-4-5 + features: + advancedStealth: true # "Verified" — DEFAULT ON (the #1 trial mistake is leaving it off) + proxies: true # residential proxies — pair with stealth + solveCaptchas: true + # proxyCountry: BR # geo-target the proxy if the site checks location + +sites: + - name: "Login flow" + url: "https://portal.example.com/login" + task: "Log in with the provided credentials and confirm the dashboard loads." + expect: "The account dashboard is visible after login." + antibot: akamai # informational + target: 0.85 +``` + +That's it. No pipeline code, no logging setup, no concurrency plumbing. + +--- + +## What you get + +**`scorecard.md` — the acceptance matrix** (the artifact CEs build by hand): + +| Site | Success | Target | Result | Top failure | Anti-bot seen | +| ---------- | ---------- | ------ | -------- | ---------------- | ------------- | +| Login flow | 8/10 (80%) | 80% | ✅ Met | — | Akamai | +| Search | 4/10 (40%) | 80% | ❌ Below | CAPTCHA unsolved | hCaptcha | + +**`scorecard.html` — a branded, leadership-ready report** with per-site donuts, +the exact feature posture used, and a **replayable Browserbase session link for +every single attempt** (pass _and_ fail) — the evidence customers actually want. + +**`results.json`** — raw data for your own dashboards. + +--- + +## Commands + +| Command | What it does | +| -------------- | ---------------------------------------------- | +| `bbpoc init` | Scaffold `trial.yaml` + `.env` | +| `bbpoc smoke` | Quick sanity run, 1 attempt/site | +| `bbpoc run` | Full trial per `trial.yaml` | +| `bbpoc report` | Rebuild reports from a previous `results.json` | + +**Useful flags** (on `run` / `smoke`): + +| Flag | Purpose | +| ------------------------------------------- | ----------------------------------------- | +| `--preset verified\|baseline\|stealth-only` | Flip the whole feature posture | +| `--concurrency ` | e.g. `--concurrency 100` for a scale test | +| `--attempts ` | Override attempts per site | +| `--no-stealth` / `--no-proxies` | Run a control to prove the lift | +| `-c, --config ` | Use a different manifest | + +**Prove the value of stealth** by running the same manifest twice: + +```bash +npx bbpoc run --preset baseline -o results/baseline # stealth OFF +npx bbpoc run --preset verified -o results/verified # stealth ON +``` + +…then compare the two `scorecard.html` files side by side. (Customers routinely +see jumps like 30% → 80% once Verified + proxies are on.) + +--- + +## How it decides pass vs. fail + +For each attempt, `bbpoc`: + +1. Opens the URL in a fresh Browserbase session (fresh session = fresh proxy IP — + the "retry to rotate the proxy" pattern, built in). +2. Drives the task with a Stagehand agent. +3. Scrapes the page and **classifies the outcome**: `pass`, `blocked (anti-bot)`, + `CAPTCHA unsolved`, `account/OTP wall`, `timeout`, or `error` — and names the + vendor it detected (Cloudflare, Akamai, PerimeterX, DataDome, hCaptcha, …). + +So a failure tells you _why_: "you weren't blocked by Browserbase, you hit +Akamai" vs. "the agent ran out of steps." That distinction is the whole game. + +--- + +## The skill is the front door + +`.claude/skills/bbpoc/` contains the skill that drives everything from plain +English, plus a `FAQ.md`. Because it's a project skill, it's available the moment +you open this repo in Claude Code. It can also answer "what's a context?", "why is +this site still blocked?", and "how do I geo-target a proxy?" — so your trial +rarely needs a support ticket. Copy the folder into `~/.claude/skills/` to use it +everywhere. + +--- + +## Requirements + +- Node ≥ 20 +- A Browserbase account (`BROWSERBASE_API_KEY`, `BROWSERBASE_PROJECT_ID`) +- A model key: `ANTHROPIC_API_KEY` (default), `OPENAI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY` + +Built on [Browserbase](https://browserbase.com) + [Stagehand](https://github.com/browserbase/stagehand). diff --git a/packages/examples/demos/configurable-browser-trial/bin/bbpoc.mjs b/packages/examples/demos/configurable-browser-trial/bin/bbpoc.mjs new file mode 100644 index 0000000000..d8aa05db25 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/bin/bbpoc.mjs @@ -0,0 +1,28 @@ +#!/usr/bin/env node +// Thin launcher so `bbpoc ...` runs the TypeScript CLI via tsx with no build step. +import { spawn } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { dirname, join } from "node:path"; +import { createRequire } from "node:module"; + +const here = dirname(fileURLToPath(import.meta.url)); +const cli = join(here, "..", "src", "cli.ts"); + +// Resolve tsx from THIS package's node_modules so the launcher works from any cwd. +const require = createRequire(import.meta.url); +let tsxImport = "tsx"; +try { + tsxImport = pathToFileURL(require.resolve("tsx")).href; +} catch { + /* fall back to bare specifier; resolves when run inside the installed repo */ +} + +const child = spawn(process.execPath, ["--import", tsxImport, cli, ...process.argv.slice(2)], { + stdio: "inherit", +}); +child.on("exit", (code) => process.exit(code ?? 0)); +child.on("error", (err) => { + console.error("Failed to launch bbpoc:", err.message); + console.error("Did you run `npm install`?"); + process.exit(1); +}); diff --git a/packages/examples/demos/configurable-browser-trial/examples/trial.example.yaml b/packages/examples/demos/configurable-browser-trial/examples/trial.example.yaml new file mode 100644 index 0000000000..1efe2793b4 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/examples/trial.example.yaml @@ -0,0 +1,47 @@ +# ───────────────────────────────────────────────────────────────────────────── +# bbpoc trial manifest — THE ONLY FILE YOU NEED TO EDIT +# +# Drop in your own protected URLs + what success looks like, then run: +# bbpoc smoke # quick sanity (1 attempt/site) +# bbpoc run # full trial +# ───────────────────────────────────────────────────────────────────────────── + +name: "Acme — Browserbase Verified Trial" +customer: "Acme Inc." + +defaults: + attempts: 3 # run EACH site N times — stability over N runs is the real bar, not 1-shot + concurrency: 5 # how many sessions run at once + region: us-west-2 # us-west-2 | us-east-1 | eu-central-1 | ap-southeast-1 + target: 0.8 # default per-site success target (0–1) + model: anthropic/claude-sonnet-4-5 + features: + advancedStealth: true # "Verified" — DEFAULT ON. The #1 trial mistake is leaving this off. + proxies: true # residential proxies — pair with stealth ("it's a must") + solveCaptchas: true # in-house Cloudflare / hCaptcha / reCAPTCHA / FunCaptcha solvers + blockAds: true + # proxyCountry: US # set if the site geo-checks the proxy (e.g. BR, MX, GB) + +sites: + # Each site: where to start, the task in plain English, and what "success" means. + - name: "Example login" + url: "https://www.example.com/" + task: "Confirm the page loads and read the main heading." + expect: "The page rendered and the main heading text is visible." + target: 0.9 + # antibot: cloudflare # optional: the vendor you expect to face (informational) + + - name: "Product page" + url: "https://books.toscrape.com/" + task: "Find the first book on the page and read its title and price." + expect: "A book title and a price (e.g. £51.77) are visible." + target: 0.9 + + # - name: "Your protected portal" + # url: "https://portal.example.com/login" + # task: "Log in with the provided credentials and confirm the dashboard loads." + # expect: "The account dashboard / home screen is visible after login." + # antibot: akamai + # target: 0.8 + # features: + # proxyCountry: US # per-site overrides are allowed diff --git a/packages/examples/demos/configurable-browser-trial/package.json b/packages/examples/demos/configurable-browser-trial/package.json new file mode 100644 index 0000000000..ae530e6b97 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/package.json @@ -0,0 +1,34 @@ +{ + "name": "@browserbasehq/bbpoc", + "version": "0.1.0", + "description": "Turn a 1-week Browserbase verified/advanced-stealth trial into a 1-day, self-serve smoke test. Edit one file, run one command, get a leadership-ready scorecard.", + "bin": { + "bbpoc": "bin/bbpoc.mjs" + }, + "type": "module", + "scripts": { + "bbpoc": "node --import tsx src/cli.ts", + "init": "node --import tsx src/cli.ts init", + "smoke": "node --import tsx src/cli.ts smoke", + "run": "node --import tsx src/cli.ts run", + "report": "node --import tsx src/cli.ts report", + "test": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@browserbasehq/sdk": "^2.6.0", + "@browserbasehq/stagehand": "^2.4.0", + "commander": "^12.1.0", + "picocolors": "^1.1.1", + "yaml": "^2.6.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/examples/demos/configurable-browser-trial/src/classify.ts b/packages/examples/demos/configurable-browser-trial/src/classify.ts new file mode 100644 index 0000000000..a9b1d196c3 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/classify.ts @@ -0,0 +1,145 @@ +import type { Outcome } from "./types.js"; + +/** + * Signatures of common anti-bot / challenge pages. We classify a failure as an + * anti-bot block (vs. agent logic or a timeout) so the scorecard tells the + * customer the truth: "you weren't blocked by us, you were blocked by Akamai." + */ +const VENDOR_SIGNATURES: { vendor: string; patterns: RegExp[] }[] = [ + { + vendor: "Cloudflare", + patterns: [ + /cloudflare/i, + /cf-chl/i, + /attention required/i, + /checking your browser/i, + /cf-ray/i, + ], + }, + { vendor: "Akamai", patterns: [/akamai/i, /reference #\d+\.\w+/i, /access denied.*akamai/i] }, + { + vendor: "PerimeterX / HUMAN", + patterns: [/perimeterx/i, /px-captcha/i, /press (?:and|&) hold/i, /human challenge/i], + }, + { vendor: "DataDome", patterns: [/datadome/i, /geo\.captcha-delivery/i] }, + { vendor: "Kasada", patterns: [/kasada/i, /kpsdk/i] }, + { vendor: "reCAPTCHA", patterns: [/recaptcha/i, /i'?m not a robot/i, /g-recaptcha/i] }, + { vendor: "hCaptcha", patterns: [/hcaptcha/i] }, + { vendor: "FunCaptcha / Arkose", patterns: [/funcaptcha/i, /arkose/i] }, + { vendor: "Shape / F5", patterns: [/shape security/i, /imperva/i] }, +]; + +const CAPTCHA_HINTS = [ + /captcha/i, + /verify you are (?:a )?human/i, + /are you a robot/i, + /complete the security check/i, +]; +const BLOCK_HINTS = [ + /access denied/i, + /forbidden/i, + /403/i, + /unusual traffic/i, + /blocked/i, + /bot detected/i, + /request blocked/i, + /you have been blocked/i, +]; +const WALL_HINTS = [ + /sign in/i, + /log in to continue/i, + /create (?:an )?account/i, + /one[- ]time (?:pass)?code/i, + /enter the code/i, + /verification code/i, + /please log in/i, +]; + +export interface PageSignals { + title: string; + url: string; + /** A chunk of body text (first ~4k chars is plenty). */ + text: string; +} + +/** Returns the detected anti-bot / captcha vendor on the page, if any. */ +export function detectVendor(s: PageSignals): string | undefined { + const hay = `${s.title}\n${s.url}\n${s.text}`; + for (const { vendor, patterns } of VENDOR_SIGNATURES) { + if (patterns.some((p) => p.test(hay))) return vendor; + } + return undefined; +} + +function anyMatch(s: PageSignals, hints: RegExp[]): boolean { + const hay = `${s.title}\n${s.text}`; + return hints.some((p) => p.test(hay)); +} + +/** + * Decide the outcome of an attempt from (a) the model's success judgment and + * (b) hard signals scraped off the page. Page signals win for failures so we + * can attribute the failure to a vendor rather than vague "agent failed". + */ +export function classify(opts: { + graderSuccess: boolean; + graderBlocked: boolean; + timedOut: boolean; + errored: boolean; + signals?: PageSignals; +}): { outcome: Outcome; detected?: string; reason: string } { + const { graderSuccess, graderBlocked, timedOut, errored, signals } = opts; + const detected = signals ? detectVendor(signals) : undefined; + + if (errored && !signals) { + return { + outcome: "error", + reason: "Session/runtime error before the page could be evaluated.", + }; + } + + if (graderSuccess && !graderBlocked) { + return { outcome: "pass", detected, reason: "Task completed and success criteria met." }; + } + + // Failure path — attribute it as specifically as possible. + if (signals) { + if (anyMatch(signals, CAPTCHA_HINTS) || /captcha/i.test(detected ?? "")) { + return { + outcome: "captcha_unsolved", + detected, + reason: `CAPTCHA challenge not solved${detected ? ` (${detected})` : ""}.`, + }; + } + if (graderBlocked || anyMatch(signals, BLOCK_HINTS)) { + return { + outcome: "blocked_antibot", + detected, + reason: `Blocked by anti-bot${detected ? ` (${detected})` : ""}.`, + }; + } + if (anyMatch(signals, WALL_HINTS)) { + return { + outcome: "account_wall", + detected, + reason: "Stopped at a login / OTP / account wall.", + }; + } + } + + if (timedOut) { + return { + outcome: "timeout", + detected, + reason: "Ran out of steps / time before completing the task.", + }; + } + if (graderBlocked) { + return { + outcome: "blocked_antibot", + detected, + reason: `Blocked by anti-bot${detected ? ` (${detected})` : ""}.`, + }; + } + return { outcome: "error", detected, reason: "Task did not meet success criteria." }; +} diff --git a/packages/examples/demos/configurable-browser-trial/src/cli.ts b/packages/examples/demos/configurable-browser-trial/src/cli.ts new file mode 100644 index 0000000000..dbbc1fc296 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/cli.ts @@ -0,0 +1,201 @@ +import { Command } from "commander"; +import { mkdirSync, writeFileSync, existsSync, copyFileSync, readFileSync } from "node:fs"; +import { resolve, join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import pc from "picocolors"; +import { loadManifest, checkEnv, applyPreset } from "./config.js"; +import { runAttempt } from "./runner.js"; +import { pool } from "./pool.js"; +import { buildScorecard, OUTCOME_LABELS } from "./scorecard.js"; +import { renderMarkdown } from "./report/markdown.js"; +import { renderHtml } from "./report/html.js"; +import type { AttemptResult, Manifest, Scorecard } from "./types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ROOT = resolve(__dirname, ".."); + +// Best-effort .env loader (no dotenv dep — keep the template lean). +function loadDotenv() { + const p = resolve(process.cwd(), ".env"); + if (!existsSync(p)) return; + for (const line of readFileSync(p, "utf8").split("\n")) { + const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/); + if (m && !process.env[m[1]]) { + process.env[m[1]] = m[2].replace(/^["']|["']$/g, ""); + } + } +} +loadDotenv(); + +const ts = () => new Date().toISOString().replace("T", " ").slice(0, 19); +const banner = () => + console.log(pc.bold(pc.red("\n bbpoc")) + pc.dim(" — Browserbase verified-trial harness\n")); + +/** Build the flat task list (site × attempts) and run it through the pool. */ +async function executeTrial(manifest: Manifest, opts: { live: boolean }): Promise { + const startedAt = ts(); + const jobs: { siteIndex: number; attempt: number }[] = []; + manifest.sites.forEach((s, i) => { + const n = s.attempts ?? manifest.defaults.attempts; + for (let a = 1; a <= n; a++) jobs.push({ siteIndex: i, attempt: a }); + }); + + console.log( + pc.dim( + ` ${manifest.sites.length} sites × attempts = ${jobs.length} sessions · ` + + `concurrency ${manifest.defaults.concurrency} · region ${manifest.defaults.region}`, + ), + ); + console.log( + pc.dim( + ` stealth ${manifest.defaults.features.advancedStealth ? "ON" : "OFF"} · ` + + `proxies ${manifest.defaults.features.proxies ? "ON" : "OFF"}` + + `${manifest.defaults.features.proxyCountry ? ` (${manifest.defaults.features.proxyCountry})` : ""} · ` + + `captcha ${manifest.defaults.features.solveCaptchas ? "ON" : "OFF"}\n`, + ), + ); + + let done = 0; + const results = await pool<(typeof jobs)[number], AttemptResult>( + jobs, + manifest.defaults.concurrency, + (job) => runAttempt(manifest.sites[job.siteIndex], manifest.defaults, job.attempt), + (r) => { + done++; + const icon = r.success ? pc.green("✓") : pc.red("✗"); + const tag = r.success + ? pc.green(OUTCOME_LABELS[r.outcome]) + : pc.yellow(OUTCOME_LABELS[r.outcome]); + console.log( + ` ${icon} [${String(done).padStart(2)}/${jobs.length}] ${pc.bold(r.site)} #${r.attempt} — ${tag}` + + (r.detected ? pc.dim(` · ${r.detected}`) : "") + + pc.dim(` · ${(r.durationMs / 1000).toFixed(1)}s`), + ); + }, + ); + + return buildScorecard(manifest, results, startedAt, ts()); +} + +function writeReports(s: Scorecard, outDir: string) { + mkdirSync(outDir, { recursive: true }); + const md = join(outDir, "scorecard.md"); + const html = join(outDir, "scorecard.html"); + const jsonPath = join(outDir, "results.json"); + writeFileSync(md, renderMarkdown(s)); + writeFileSync(html, renderHtml(s)); + writeFileSync(jsonPath, JSON.stringify(s, null, 2)); + console.log("\n" + pc.bold(" Reports written:")); + console.log(" " + pc.cyan(md) + pc.dim(" (acceptance matrix)")); + console.log(" " + pc.cyan(html) + pc.dim(" (leadership-ready, open in a browser)")); + console.log(" " + pc.cyan(jsonPath) + pc.dim(" (raw)")); +} + +function printSummary(s: Scorecard) { + console.log("\n" + pc.bold(" ── Scorecard ──")); + for (const site of s.sites) { + const ok = site.met ? pc.green("MET ") : pc.red("MISS"); + const rate = `${Math.round(site.successRate * 100)}%`.padStart(4); + console.log( + ` ${ok} ${rate} ${pc.bold(site.name)} ${pc.dim(`(target ${Math.round(site.target * 100)}%)`)}`, + ); + } + const all = s.sitesMet === s.siteCount; + console.log( + "\n " + + (all + ? pc.green(pc.bold(`✓ ${s.sitesMet}/${s.siteCount} sites met target`)) + : pc.yellow(pc.bold(`${s.sitesMet}/${s.siteCount} sites met target`))) + + pc.dim(` · ${Math.round(s.overallRate * 100)}% overall`), + ); +} + +const program = new Command(); +program + .name("bbpoc") + .description( + "Turn a Browserbase verified/advanced-stealth trial into a one-day self-serve smoke test.", + ) + .version("0.1.0"); + +program + .command("init") + .description("Scaffold trial.yaml + .env in the current directory") + .action(() => { + banner(); + const targets: [string, string][] = [ + [join(ROOT, "examples", "trial.example.yaml"), "trial.yaml"], + [join(ROOT, ".env.example"), ".env"], + ]; + for (const [src, dest] of targets) { + const out = resolve(process.cwd(), dest); + if (existsSync(out)) { + console.log(pc.yellow(` • ${dest} already exists — skipped`)); + continue; + } + copyFileSync(src, out); + console.log(pc.green(` ✓ created ${dest}`)); + } + console.log( + "\n Next:\n" + + pc.dim(" 1. fill in .env with your Browserbase + model keys\n") + + pc.dim(" 2. edit trial.yaml — your URLs, tasks, targets\n") + + pc.dim(" 3. ") + + pc.cyan("bbpoc smoke") + + pc.dim(" (quick sanity, 1 attempt/site)\n") + + pc.dim(" 4. ") + + pc.cyan("bbpoc run") + + pc.dim(" (full trial)\n"), + ); + }); + +function trialCommand(name: string, smoke: boolean) { + program + .command(name) + .description( + smoke ? "Quick sanity run — 1 attempt per site" : "Run the full trial per trial.yaml", + ) + .option("-c, --config ", "path to manifest", "trial.yaml") + .option("-o, --out ", "output directory", "results") + .option("--preset ", "feature preset: verified | baseline | stealth-only") + .option("--concurrency ", "override concurrency", (v) => parseInt(v, 10)) + .option("--attempts ", "override attempts per site", (v) => parseInt(v, 10)) + .option("--no-stealth", "disable advanced stealth (control run)") + .option("--no-proxies", "disable residential proxies") + .action(async (opts) => { + banner(); + let manifest = loadManifest(opts.config); + manifest = applyPreset(manifest, opts.preset); + if (smoke) manifest.defaults.attempts = 1; + if (opts.attempts) manifest.defaults.attempts = opts.attempts; + if (opts.concurrency) manifest.defaults.concurrency = opts.concurrency; + if (opts.stealth === false) manifest.defaults.features.advancedStealth = false; + if (opts.proxies === false) manifest.defaults.features.proxies = false; + + checkEnv(manifest.defaults.model); + + const s = await executeTrial(manifest, { live: true }); + printSummary(s); + writeReports(s, resolve(process.cwd(), opts.out)); + }); +} +trialCommand("run", false); +trialCommand("smoke", true); + +program + .command("report") + .description("Regenerate markdown + HTML from a previous results.json") + .option("-i, --in ", "results.json path", "results/results.json") + .option("-o, --out ", "output directory", "results") + .action((opts) => { + banner(); + const p = resolve(process.cwd(), opts.in); + if (!existsSync(p)) { + console.error(pc.red(`✖ ${opts.in} not found. Run a trial first.`)); + process.exit(1); + } + const s = JSON.parse(readFileSync(p, "utf8")) as Scorecard; + writeReports(s, resolve(process.cwd(), opts.out)); + }); + +program.parseAsync(process.argv); diff --git a/packages/examples/demos/configurable-browser-trial/src/config.ts b/packages/examples/demos/configurable-browser-trial/src/config.ts new file mode 100644 index 0000000000..29644594df --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/config.ts @@ -0,0 +1,96 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { parse } from "yaml"; +import pc from "picocolors"; +import { ManifestSchema, type Manifest, type Features } from "./types.js"; + +/** Resolve which model API key env var to use for a given "provider/model" id. */ +export function resolveModelKey(model: string): { key: string; envVar: string } | null { + const provider = model.split("/")[0]; + const map: Record = { + anthropic: "ANTHROPIC_API_KEY", + openai: "OPENAI_API_KEY", + google: "GOOGLE_GENERATIVE_AI_API_KEY", + gemini: "GOOGLE_GENERATIVE_AI_API_KEY", + }; + const envVar = map[provider]; + if (envVar && process.env[envVar]) return { key: process.env[envVar]!, envVar }; + if (process.env.STAGEHAND_MODEL_API_KEY) + return { key: process.env.STAGEHAND_MODEL_API_KEY, envVar: "STAGEHAND_MODEL_API_KEY" }; + return null; +} + +/** Load + validate the trial manifest. Exits with a friendly message on error. */ +export function loadManifest(path: string): Manifest { + const abs = resolve(process.cwd(), path); + let raw: string; + try { + raw = readFileSync(abs, "utf8"); + } catch { + console.error(pc.red(`✖ Could not read manifest at ${abs}`)); + console.error(pc.dim(` Run ${pc.cyan("bbpoc init")} to scaffold one.`)); + process.exit(1); + } + let data: unknown; + try { + data = parse(raw); + } catch (e) { + console.error(pc.red(`✖ ${path} is not valid YAML:`), (e as Error).message); + process.exit(1); + } + const parsed = ManifestSchema.safeParse(data); + if (!parsed.success) { + console.error(pc.red(`✖ ${path} has invalid fields:`)); + for (const issue of parsed.error.issues) { + console.error(pc.dim(` • ${issue.path.join(".") || "(root)"}: ${issue.message}`)); + } + process.exit(1); + } + return parsed.data; +} + +/** Verify the required env is present before we spend money on sessions. */ +export function checkEnv(model: string): void { + const missing: string[] = []; + if (!process.env.BROWSERBASE_API_KEY) missing.push("BROWSERBASE_API_KEY"); + if (!process.env.BROWSERBASE_PROJECT_ID) missing.push("BROWSERBASE_PROJECT_ID"); + if (missing.length) { + console.error(pc.red(`✖ Missing required env: ${missing.join(", ")}`)); + console.error(pc.dim(" Copy .env.example → .env and fill it in.")); + process.exit(1); + } + if (!resolveModelKey(model)) { + console.error(pc.red(`✖ No model API key found for "${model}".`)); + console.error( + pc.dim(" Set ANTHROPIC_API_KEY (default), OPENAI_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY."), + ); + process.exit(1); + } +} + +/** Merge manifest defaults with per-site feature overrides. */ +export function effectiveFeatures(base: Features, override?: Partial): Features { + return { ...base, ...(override ?? {}) }; +} + +/** Apply a named preset to the parsed manifest (CLI sugar like `--preset verified`). */ +export function applyPreset(manifest: Manifest, preset?: string): Manifest { + if (!preset) return manifest; + const presets: Record> = { + // Full advanced-stealth posture — the canonical enterprise trial. + verified: { advancedStealth: true, proxies: true, solveCaptchas: true }, + // Baseline cloud browser, no stealth — useful as an A/B control. + baseline: { advancedStealth: false, proxies: false, solveCaptchas: false }, + // Stealth without proxies (rare, but customers ask). + "stealth-only": { advancedStealth: true, proxies: false }, + }; + const p = presets[preset]; + if (!p) { + console.error( + pc.red(`✖ Unknown preset "${preset}". Options: ${Object.keys(presets).join(", ")}`), + ); + process.exit(1); + } + manifest.defaults.features = { ...manifest.defaults.features, ...p }; + return manifest; +} diff --git a/packages/examples/demos/configurable-browser-trial/src/pool.ts b/packages/examples/demos/configurable-browser-trial/src/pool.ts new file mode 100644 index 0000000000..0d8515d344 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/pool.ts @@ -0,0 +1,24 @@ +/** Run async tasks with a bounded concurrency limit, preserving input order. */ +export async function pool( + items: T[], + limit: number, + worker: (item: T, index: number) => Promise, + onResult?: (result: R, item: T, index: number) => void, +): Promise { + const results = new Array(items.length); + let next = 0; + const size = Math.max(1, Math.min(limit, items.length)); + + async function runner() { + while (true) { + const i = next++; + if (i >= items.length) return; + const r = await worker(items[i], i); + results[i] = r; + onResult?.(r, items[i], i); + } + } + + await Promise.all(Array.from({ length: size }, runner)); + return results; +} diff --git a/packages/examples/demos/configurable-browser-trial/src/report/html.ts b/packages/examples/demos/configurable-browser-trial/src/report/html.ts new file mode 100644 index 0000000000..a47235c418 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/report/html.ts @@ -0,0 +1,156 @@ +import { OUTCOME_LABELS } from "../scorecard.js"; +import type { Outcome, Scorecard, SiteScore } from "../types.js"; + +// Official Browserbase brand palette. +const C = { + primary: "#F03603", + black: "#100D0D", + gray: "#514F4F", + white: "#F9F6F4", + blue: "#4DA9E4", + yellow: "#F4BA41", + green: "#90C94D", + border: "#edebeb", +}; + +const OUTCOME_COLOR: Record = { + pass: C.green, + blocked_antibot: C.primary, + captcha_unsolved: C.primary, + account_wall: C.yellow, + timeout: C.yellow, + error: C.gray, +}; + +const pct = (n: number) => `${Math.round(n * 100)}%`; +const esc = (s: string) => + s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + +function donut(rate: number, met: boolean): string { + const color = met ? C.green : rate >= 0.5 ? C.yellow : C.primary; + const deg = Math.round(rate * 360); + return `
+
${pct(rate)}
+
`; +} + +function featureChips(s: Scorecard): string { + const f = s.features; + const chip = (label: string, on: boolean) => + `${on ? "✓" : "✕"} ${label}`; + return [ + chip("Advanced Stealth", f.advancedStealth), + chip(`Residential Proxies${f.proxyCountry ? ` · ${f.proxyCountry}` : ""}`, f.proxies), + chip("CAPTCHA Solving", f.solveCaptchas), + chip("Ad Blocking", f.blockAds), + ].join(""); +} + +function siteCard(site: SiteScore): string { + const rows = site.results + .map((r) => { + const color = OUTCOME_COLOR[r.outcome]; + const replay = r.replayUrl + ? `▶ replay` + : "—"; + return ` + ${r.attempt} + ${OUTCOME_LABELS[r.outcome]} + ${esc(r.reason)} + ${replay} + `; + }) + .join(""); + + const seen = site.detected.length ? site.detected.join(", ") : (site.antibot ?? "—"); + return `
+
+ ${donut(site.successRate, site.met)} +
+

${esc(site.name)} ${site.met ? `TARGET MET` : `BELOW TARGET`}

+
${esc(site.task)}
+
${site.passes}/${site.attempts} passed · target ${pct(site.target)} · anti-bot: ${esc(seen)}
+ ${esc(site.url)} +
+
+ + + ${rows} +
#OutcomeReasonSession
+
`; +} + +/** A clean, on-brand, single-file report a customer can forward to their CTO. */ +export function renderHtml(s: Scorecard): string { + const title = s.customer ? `${esc(s.customer)} — ${esc(s.name)}` : esc(s.name); + return ` + + + + +${title} + + + +
+
+
Browserbase · Verified Trial Scorecard
+

${title}

+
${esc(s.startedAt)} → ${esc(s.finishedAt)} · ${esc(s.region)} · ${esc(s.model)}
+
${featureChips(s)}
+
+ +
+
${s.sitesMet}/${s.siteCount}
Sites hit target
+
${pct(s.overallRate)}
Overall success
+
${s.totalPasses}/${s.totalAttempts}
Attempts passed
+
+ +

Per-site results

+ ${s.sites.map(siteCard).join("\n")} + +
+ Generated by bbpoc — the Browserbase verified-trial harness. Each session above is a real, replayable cloud-browser run. +
+
+ +`; +} diff --git a/packages/examples/demos/configurable-browser-trial/src/report/markdown.ts b/packages/examples/demos/configurable-browser-trial/src/report/markdown.ts new file mode 100644 index 0000000000..08b8963229 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/report/markdown.ts @@ -0,0 +1,75 @@ +import { OUTCOME_LABELS } from "../scorecard.js"; +import type { Scorecard } from "../types.js"; + +const pct = (n: number) => `${Math.round(n * 100)}%`; + +function featureLine(s: Scorecard): string { + const f = s.features; + const on = (b: boolean) => (b ? "✅" : "❌"); + return [ + `Advanced Stealth ${on(f.advancedStealth)}`, + `Residential Proxies ${on(f.proxies)}${f.proxyCountry ? ` (${f.proxyCountry})` : ""}`, + `CAPTCHA Solving ${on(f.solveCaptchas)}`, + `Ad Blocking ${on(f.blockAds)}`, + ].join(" · "); +} + +/** The "acceptance matrix" CEs build by hand — one row per site, machine-made. */ +export function renderMarkdown(s: Scorecard): string { + const lines: string[] = []; + lines.push(`# ${s.name} — Results Scorecard`); + if (s.customer) lines.push(`**Customer:** ${s.customer} `); + lines.push(`**Run:** ${s.startedAt} → ${s.finishedAt} `); + lines.push(`**Region:** ${s.region} · **Model:** ${s.model} `); + lines.push(`**Config:** ${featureLine(s)}`); + lines.push(""); + + // Headline. + const headline = + `> **${s.sitesMet}/${s.siteCount} sites hit their target.** ` + + `Overall ${pct(s.overallRate)} success across ${s.totalAttempts} attempts.`; + lines.push(headline); + lines.push(""); + + // Acceptance matrix. + lines.push("## Acceptance Matrix"); + lines.push(""); + lines.push("| Site | Task | Success | Target | Result | Top failure | Anti-bot seen |"); + lines.push("|------|------|---------|--------|--------|-------------|---------------|"); + for (const site of s.sites) { + const result = site.met ? "✅ Met" : "❌ Below"; + const fail = site.topFailure ? OUTCOME_LABELS[site.topFailure] : "—"; + const seen = site.detected.length ? site.detected.join(", ") : (site.antibot ?? "—"); + lines.push( + `| ${site.name} | ${escapeCell(site.task)} | ${site.passes}/${site.attempts} (${pct(site.successRate)}) | ${pct(site.target)} | ${result} | ${fail} | ${seen} |`, + ); + } + lines.push(""); + + // Per-site detail with session replay links (the evidence customers want). + lines.push("## Session Detail"); + for (const site of s.sites) { + lines.push(""); + lines.push(`### ${site.name} — ${pct(site.successRate)} (${site.passes}/${site.attempts})`); + lines.push(""); + lines.push("| # | Outcome | Reason | Replay |"); + lines.push("|---|---------|--------|--------|"); + for (const r of site.results) { + const icon = r.success ? "🟢" : "🔴"; + const replay = r.replayUrl ? `[session](${r.replayUrl})` : "—"; + lines.push( + `| ${r.attempt} | ${icon} ${OUTCOME_LABELS[r.outcome]} | ${escapeCell(r.reason)} | ${replay} |`, + ); + } + } + lines.push(""); + lines.push("---"); + lines.push( + `_Generated by [bbpoc](https://github.com/browserbase/bbpoc) · Browserbase verified-trial harness._`, + ); + return lines.join("\n"); +} + +function escapeCell(s: string): string { + return s.replace(/\|/g, "\\|").replace(/\n/g, " ").slice(0, 160); +} diff --git a/packages/examples/demos/configurable-browser-trial/src/runner.ts b/packages/examples/demos/configurable-browser-trial/src/runner.ts new file mode 100644 index 0000000000..551fa97da3 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/runner.ts @@ -0,0 +1,169 @@ +import { Stagehand } from "@browserbasehq/stagehand"; +import { z } from "zod"; +import { classify, type PageSignals } from "./classify.js"; +import { resolveModelKey, effectiveFeatures } from "./config.js"; +import type { AttemptResult, Defaults, Features, Site } from "./types.js"; + +const REPLAY_BASE = "https://www.browserbase.com/sessions"; + +/** Build the Browserbase session create params from the resolved feature flags. */ +function sessionParams(features: Features, region: string, projectId: string) { + const browserSettings: Record = { + advancedStealth: features.advancedStealth, + blockAds: features.blockAds, + solveCaptchas: features.solveCaptchas, + }; + // Geo-targeted residential proxies need the array form; plain `true` otherwise. + let proxies: unknown = features.proxies; + if (features.proxies && features.proxyCountry) { + proxies = [{ type: "browserbase", geolocation: { country: features.proxyCountry } }]; + } + return { projectId, region, proxies, browserSettings, keepAlive: false }; +} + +const GraderSchema = z.object({ + success: z.boolean().describe("True only if the stated goal was clearly achieved."), + blocked: z + .boolean() + .describe("True if the page shows bot-detection, access-denied, or an unsolved CAPTCHA."), + evidence: z.string().describe("One sentence of evidence for the judgment."), +}); + +async function captureSignals(page: any): Promise { + try { + const [title, text] = await Promise.all([ + page.title().catch(() => ""), + page.evaluate(() => (document.body?.innerText || "").slice(0, 4000)).catch(() => ""), + ]); + return { title, url: page.url(), text }; + } catch { + return undefined; + } +} + +function withTimeout(p: Promise, ms: number): Promise { + return Promise.race([ + p, + new Promise<"__timeout__">((res) => setTimeout(() => res("__timeout__"), ms)), + ]); +} + +/** + * Run ONE attempt against ONE site in a fresh session (= fresh proxy IP, which + * is exactly the "retry to rotate the proxy" pattern CEs recommend). + */ +export async function runAttempt( + site: Site, + defaults: Defaults, + attempt: number, +): Promise { + const start = Date.now(); + const name = site.name ?? new URL(site.url).hostname.replace(/^www\./, ""); + const features = effectiveFeatures(defaults.features, site.features); + const expect = site.expect ?? site.task; + const model = defaults.model; + const modelKey = resolveModelKey(model)!; + + const base: Omit = { + site: name, + url: site.url, + attempt, + durationMs: 0, + }; + + let stagehand: Stagehand | null = null; + try { + stagehand = new Stagehand({ + env: "BROWSERBASE", + apiKey: process.env.BROWSERBASE_API_KEY!, + projectId: process.env.BROWSERBASE_PROJECT_ID!, + modelName: model, + modelClientOptions: { apiKey: modelKey.key }, + verbose: 0, + browserbaseSessionCreateParams: sessionParams( + features, + defaults.region, + process.env.BROWSERBASE_PROJECT_ID!, + ) as any, + }); + + await stagehand.init(); + const sessionId = (stagehand as any).browserbaseSessionID as string | undefined; + const replayUrl = sessionId ? `${REPLAY_BASE}/${sessionId}` : undefined; + const page = stagehand.page; + + // Drive the task, bounded by the per-attempt time budget. + let timedOut = false; + const work = (async () => { + await page.goto(site.url, { waitUntil: "domcontentloaded" }); + const agent = stagehand!.agent(); + await agent.execute({ instruction: site.task, maxSteps: defaults.maxSteps }); + })(); + + const raced = await withTimeout(work, defaults.timeoutMs); + if (raced === "__timeout__") timedOut = true; + + const signals = await captureSignals(page); + + // Grade the result with the model (separate from the doing). + let graderSuccess = false; + let graderBlocked = false; + try { + const verdict = await withTimeout( + page.extract({ + instruction: + `Goal: "${expect}".\n` + + `Judge ONLY from the current page. Did the goal succeed? ` + + `Set blocked=true if you see bot-detection, access-denied, or an unsolved CAPTCHA.`, + schema: GraderSchema, + }), + 30_000, + ); + if (verdict !== "__timeout__") { + graderSuccess = verdict.success; + graderBlocked = verdict.blocked; + } + } catch { + /* grading failed; classifier falls back to page signals */ + } + + const { outcome, detected, reason } = classify({ + graderSuccess, + graderBlocked, + timedOut, + errored: false, + signals, + }); + + return { + ...base, + outcome, + success: outcome === "pass", + reason, + detected, + sessionId, + replayUrl, + durationMs: Date.now() - start, + }; + } catch (err) { + const message = (err as Error)?.message ?? String(err); + // Make the enterprise-gating failure mode unmissable. + const stealthGated = + /stealth|enterprise|not.*allowed|forbidden|plan/i.test(message) && features.advancedStealth; + return { + ...base, + outcome: "error", + success: false, + reason: stealthGated + ? `Session failed — advanced stealth is Enterprise/Scale-plan only. Ask your Browserbase contact to enable it, or run with --preset baseline. (${message})` + : `Session error: ${message}`, + durationMs: Date.now() - start, + }; + } finally { + try { + await stagehand?.close(); + } catch { + /* ignore */ + } + } +} diff --git a/packages/examples/demos/configurable-browser-trial/src/scorecard.ts b/packages/examples/demos/configurable-browser-trial/src/scorecard.ts new file mode 100644 index 0000000000..b53ca2e385 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/scorecard.ts @@ -0,0 +1,85 @@ +import type { AttemptResult, Manifest, Outcome, Scorecard, SiteScore } from "./types.js"; + +function topFailure(results: AttemptResult[]): Outcome | undefined { + const counts = new Map(); + for (const r of results) { + if (r.outcome === "pass") continue; + counts.set(r.outcome, (counts.get(r.outcome) ?? 0) + 1); + } + let best: Outcome | undefined; + let bestN = 0; + for (const [o, n] of counts) { + if (n > bestN) { + best = o; + bestN = n; + } + } + return best; +} + +/** Fold the flat attempt list into a per-site + overall scorecard. */ +export function buildScorecard( + manifest: Manifest, + attempts: AttemptResult[], + startedAt: string, + finishedAt: string, +): Scorecard { + const bySite = new Map(); + for (const a of attempts) { + const arr = bySite.get(a.url) ?? []; + arr.push(a); + bySite.set(a.url, arr); + } + + const sites: SiteScore[] = manifest.sites.map((s) => { + const results = bySite.get(s.url) ?? []; + const name = s.name ?? new URL(s.url).hostname.replace(/^www\./, ""); + const passes = results.filter((r) => r.success).length; + const successRate = results.length ? passes / results.length : 0; + const target = s.target ?? manifest.defaults.target; + const detected = [...new Set(results.map((r) => r.detected).filter(Boolean) as string[])]; + return { + name, + url: s.url, + task: s.task, + antibot: s.antibot, + attempts: results.length, + passes, + successRate, + target, + met: successRate >= target, + topFailure: topFailure(results), + detected, + results, + }; + }); + + const totalAttempts = attempts.length; + const totalPasses = attempts.filter((a) => a.success).length; + const sitesMet = sites.filter((s) => s.met).length; + + return { + name: manifest.name, + customer: manifest.customer, + startedAt, + finishedAt, + features: manifest.defaults.features, + region: manifest.defaults.region, + model: manifest.defaults.model, + sites, + totalAttempts, + totalPasses, + overallRate: totalAttempts ? totalPasses / totalAttempts : 0, + sitesMet, + siteCount: sites.length, + }; +} + +export const OUTCOME_LABELS: Record = { + pass: "Pass", + blocked_antibot: "Blocked (anti-bot)", + captcha_unsolved: "CAPTCHA unsolved", + account_wall: "Account/OTP wall", + timeout: "Timed out", + error: "Error", +}; diff --git a/packages/examples/demos/configurable-browser-trial/src/types.ts b/packages/examples/demos/configurable-browser-trial/src/types.ts new file mode 100644 index 0000000000..75cfef7400 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/src/types.ts @@ -0,0 +1,135 @@ +import { z } from "zod"; + +/** + * The feature flags that map 1:1 to what customers actually toggle during a + * Browserbase verified / advanced-stealth trial. These are the knobs CEs spend + * a week explaining over Slack — here they are defaults you can't get wrong. + */ +export const FeaturesSchema = z.object({ + /** + * Advanced stealth ("Verified"). Enterprise/Scale-plan only. Default ON — + * the single most common trial mistake is testing bot-detection WITHOUT it. + */ + advancedStealth: z.boolean().default(true), + /** Browserbase-managed residential proxies. Paired with stealth — "it's a must". */ + proxies: z.boolean().default(true), + /** In-house CAPTCHA solving (Cloudflare, hCaptcha, FunCaptcha, reCAPTCHA…). */ + solveCaptchas: z.boolean().default(true), + /** Block ads/trackers to cut noise + bandwidth. */ + blockAds: z.boolean().default(true), + /** + * ISO country code for residential proxy geo-targeting (e.g. "US", "BR", + * "MX"). Many sites surface proxy location during MFA — match the customer's. + */ + proxyCountry: z.string().optional(), +}); +export type Features = z.infer; + +export const SiteSchema = z.object({ + /** Target URL to start from. */ + url: z.string().url(), + /** Natural-language description of what the agent should accomplish. */ + task: z.string().min(1), + /** + * What "success" looks like, in plain English. Used to grade each attempt. + * If omitted, falls back to `task`. + */ + expect: z.string().optional(), + /** Friendly label for reports. Defaults to the URL hostname. */ + name: z.string().optional(), + /** Per-site success-rate target (0–1). Defaults to manifest-level target. */ + target: z.number().min(0).max(1).optional(), + /** Informational: anti-bot vendor you expect to face (cloudflare, akamai…). */ + antibot: z.string().optional(), + /** Per-site feature overrides (e.g. turn proxies off for one site). */ + features: FeaturesSchema.partial().optional(), + /** Per-site attempt-count override. */ + attempts: z.number().int().positive().optional(), +}); +export type Site = z.infer; + +export const DefaultsSchema = z.object({ + /** Times to run EACH site. Stability over N runs is the real bar, not 1-shot. */ + attempts: z.number().int().positive().default(3), + /** How many sessions to run at once. */ + concurrency: z.number().int().positive().default(5), + /** Browserbase region. */ + region: z.enum(["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"]).default("us-west-2"), + /** Default per-site success target (0–1). */ + target: z.number().min(0).max(1).default(0.8), + /** Model that drives + grades each task. */ + model: z.string().default("anthropic/claude-sonnet-4-5"), + /** Max agent steps per attempt before we call it a timeout. */ + maxSteps: z.number().int().positive().default(18), + /** Per-attempt wall-clock budget in ms. */ + timeoutMs: z.number().int().positive().default(120_000), + features: FeaturesSchema.default({}), +}); +export type Defaults = z.infer; + +export const ManifestSchema = z.object({ + /** Name of the trial — shows up on the report. */ + name: z.string().default("Browserbase Verified Trial"), + /** Customer / company name for branding the leadership report. */ + customer: z.string().optional(), + defaults: DefaultsSchema.default({}), + sites: z.array(SiteSchema).min(1), +}); +export type Manifest = z.infer; + +/** How a single attempt ended. Drives the scorecard + guardrail warnings. */ +export type Outcome = + | "pass" + | "blocked_antibot" + | "captcha_unsolved" + | "account_wall" + | "timeout" + | "error"; + +export interface AttemptResult { + site: string; + url: string; + attempt: number; + outcome: Outcome; + /** True only for `pass`. */ + success: boolean; + reason: string; + /** Anti-bot / captcha vendor detected on the page, if any. */ + detected?: string; + sessionId?: string; + /** Browserbase session replay URL — the artifact CEs paste into Slack. */ + replayUrl?: string; + durationMs: number; +} + +export interface SiteScore { + name: string; + url: string; + task: string; + antibot?: string; + attempts: number; + passes: number; + successRate: number; + target: number; + met: boolean; + /** Most common non-pass outcome — the headline failure mode. */ + topFailure?: Outcome; + detected: string[]; + results: AttemptResult[]; +} + +export interface Scorecard { + name: string; + customer?: string; + startedAt: string; + finishedAt: string; + features: Features; + region: string; + model: string; + sites: SiteScore[]; + totalAttempts: number; + totalPasses: number; + overallRate: number; + sitesMet: number; + siteCount: number; +} diff --git a/packages/examples/demos/configurable-browser-trial/tsconfig.json b/packages/examples/demos/configurable-browser-trial/tsconfig.json new file mode 100644 index 0000000000..01f4767512 --- /dev/null +++ b/packages/examples/demos/configurable-browser-trial/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/examples/demos/hacker-news-intelligence/.env.example b/packages/examples/demos/hacker-news-intelligence/.env.example new file mode 100644 index 0000000000..86fbd2b825 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/.env.example @@ -0,0 +1,12 @@ +# Browserbase Configuration +BROWSERBASE_API_KEY= +BROWSERBASE_PROJECT_ID= + +# Optional Configuration +NODE_ENV=development +LOG_LEVEL=info + +# Demo Configuration +MAX_POSTS=5 +ENABLE_CONTENT_EXTRACTION=true +TIMEOUT_MS=30000 diff --git a/packages/examples/demos/hacker-news-intelligence/.gitignore b/packages/examples/demos/hacker-news-intelligence/.gitignore new file mode 100644 index 0000000000..bea4ea60c4 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/.gitignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.*.local + +# Build outputs +dist/ +build/ + +# Logs +logs/ +*.log + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Coverage +coverage/ + +# Temporary files +.tmp/ +temp/ \ No newline at end of file diff --git a/packages/examples/demos/hacker-news-intelligence/README.md b/packages/examples/demos/hacker-news-intelligence/README.md new file mode 100644 index 0000000000..0d86c18fb7 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/README.md @@ -0,0 +1,309 @@ +# Hacker News Intelligence Demo + +> **Enterprise-grade Browserbase automation demo for news aggregation and content intelligence** + +This demonstration showcases Browserbase's cloud browser automation capabilities through an AI-powered Hacker News intelligence system. Built with Stagehand's natural language automation framework, it demonstrates how enterprises can automate complex web workflows for competitive intelligence, content research, and trend analysis. + +## 🎯 What This Demo Does + +The demo performs comprehensive Hacker News intelligence gathering: + +1. **🔍 Intelligent Post Discovery**: Navigates to Hacker News and extracts the top trending posts using AI-powered element detection +2. **📖 Content Analysis**: Automatically visits external links and extracts key insights from articles +3. **🤖 AI-Powered Categorization**: Analyzes content for business relevance, sentiment, and technical complexity +4. **📊 Executive Reporting**: Generates structured intelligence reports with actionable insights +5. **🎛️ Enterprise Configuration**: Provides customizable parameters for different use cases + +## 🏢 Business Value for Enterprise Customers + +- **Competitive Intelligence**: Monitor trending technologies and startup activities +- **Content Strategy**: Identify popular topics for content marketing and thought leadership +- **Market Research**: Track sentiment and engagement around industry topics +- **Developer Relations**: Stay current with developer community interests and concerns +- **Investment Research**: Analyze startup and technology trends for investment decisions + +## 🚀 Quick Start + +### Prerequisites + +- Node.js 18+ and npm/yarn +- Browserbase account with API key +- 5 minutes for setup + +### 1. Clone and Install + +```bash +cd packages/examples/demos/hacker-news-intelligence +npm install +``` + +### 2. Configure Environment + +```bash +cp .env.example .env +``` + +Edit `.env` with your Browserbase credentials: + +```env +BROWSERBASE_API_KEY=your_browserbase_api_key_here +BROWSERBASE_PROJECT_ID=your_project_id_here + +# Optional customization +MAX_POSTS=5 +ENABLE_CONTENT_EXTRACTION=true +LOG_LEVEL=info +``` + +### 3. Verify Setup + +```bash +npm run test +# or +npm run dev -- --health-check +``` + +### 4. Run the Demo + +```bash +npm run dev +``` + +## 📖 How to Use This Demo + +### Basic Usage + +The demo runs automatically once started: + +```bash +npm run dev +``` + +**What happens during execution:** + +1. **Initialization**: Establishes secure Browserbase session with residential proxies +2. **Navigation**: Opens Hacker News front page using cloud browser infrastructure +3. **AI Extraction**: Uses Stagehand's natural language commands to identify and extract post data +4. **Content Analysis**: Visits external links and extracts article content using intelligent parsing +5. **Intelligence Generation**: Applies AI analysis for categorization, sentiment, and business relevance scoring +6. **Report Generation**: Creates comprehensive reports in both console and JSON formats + +### Configuration Options + +Customize the demo behavior through environment variables: + +```env +# Number of posts to analyze (1-30) +MAX_POSTS=10 + +# Enable/disable full content extraction +ENABLE_CONTENT_EXTRACTION=true + +# Logging verbosity +LOG_LEVEL=debug # debug, info, warn, error + +# Request timeout +TIMEOUT_MS=45000 +``` + +### Output Formats + +The demo generates multiple output formats: + +- **Console Display**: Rich formatted output with colors and emojis +- **JSON Reports**: Structured data saved to `reports/` directory +- **Executive Summary**: Business-focused insights for stakeholders + +## 🏗️ Technical Architecture + +### Built with Enterprise-Grade Technologies + +- **Browserbase Cloud Platform**: Scalable browser automation infrastructure +- **Stagehand AI Framework**: Natural language web automation +- **TypeScript**: Type-safe development with comprehensive error handling +- **Advanced Logging**: Structured logging with multiple verbosity levels +- **Graceful Error Handling**: Robust error recovery and reporting + +### Key Components + +``` +src/ +├── main.ts # Main orchestration and CLI interface +├── extractor.ts # AI-powered content extraction logic +├── reporter.ts # Intelligence report generation +├── config.ts # Environment and runtime configuration +├── logger.ts # Enterprise logging system +├── types.ts # TypeScript type definitions +└── test.ts # Automated testing suite +``` + +### Stagehand Integration Highlights + +The demo showcases advanced Stagehand capabilities: + +```typescript +// Natural language automation +await page.act("Extract the top 5 trending posts with all metadata"); + +// Intelligent content analysis +const insights = await page.extract(` + Analyze this article and provide: + - Key business insights + - Technical complexity assessment + - Market relevance score +`); + +// Adaptive element detection +await page.observe("Check if content loaded successfully"); +``` + +## 🔧 Troubleshooting + +### Common Issues + +**Error: "BROWSERBASE_API_KEY is required"** + +- Ensure `.env` file exists with valid API credentials +- Verify API key is active in your Browserbase dashboard + +**Error: "No posts extracted"** + +- Check internet connectivity +- Verify Hacker News is accessible +- Try running with `LOG_LEVEL=debug` for detailed information + +**Slow Performance** + +- Reduce `MAX_POSTS` for faster testing +- Disable content extraction: `ENABLE_CONTENT_EXTRACTION=false` +- Check Browserbase region settings in `src/config.ts` + +### Advanced Debugging + +Enable verbose logging: + +```bash +LOG_LEVEL=debug npm run dev +``` + +Run health check: + +```bash +npm run dev -- --health-check +``` + +Check Browserbase sessions: + +- View active sessions in your Browserbase dashboard +- Use Session Inspector URLs provided in debug output +- Review Session Replay for detailed interaction analysis + +## 🎛️ Customization for Your Use Case + +### Industry-Specific Adaptations + +**Financial Services**: Focus on fintech and blockchain posts + +```typescript +// Modify src/extractor.ts +const targetCategories = ["fintech", "blockchain", "trading", "banking"]; +``` + +**Healthcare**: Monitor health tech and biotech trends + +```typescript +// Custom extraction parameters +const healthcareKeywords = ["healthtech", "biotech", "medical", "pharma"]; +``` + +**Enterprise SaaS**: Track SaaS and enterprise technology + +```typescript +// Business relevance scoring adjustments +const enterpriseWeight = 1.5; // Boost enterprise-focused content +``` + +### Scaling for Production + +**High-Volume Processing**: + +```typescript +// Parallel processing configuration +const browserbaseConfig = { + concurrent: 5, // Multiple browser sessions + rateLimiting: true, // Respect rate limits + retryLogic: 3, // Automatic retry on failures +}; +``` + +**Data Integration**: + +```typescript +// Export to external systems +await exportToSlack(report); +await saveToDatabase(analyses); +await sendToDataWarehouse(intelligenceData); +``` + +## 🔐 Security and Compliance + +This demo implements enterprise security best practices: + +- **No Data Persistence**: No sensitive data stored locally +- **Secure Sessions**: All browser sessions use Browserbase's secure infrastructure +- **Configurable Privacy**: Session recording can be disabled for compliance +- **Rate Limiting**: Respects website rate limits and robots.txt +- **Error Isolation**: Failed extractions don't impact overall execution + +## 📊 Performance Metrics + +Typical performance benchmarks: + +- **5 Posts**: ~30-45 seconds +- **10 Posts**: ~60-90 seconds +- **20 Posts**: ~120-180 seconds + +Performance factors: + +- Content extraction enabled/disabled +- Network latency to target sites +- Browserbase region selection +- Article length and complexity + +## 🚀 Next Steps for Enterprise Implementation + +### Immediate Enhancements + +1. **Scheduling**: Add cron jobs for regular intelligence gathering +2. **Notifications**: Integrate with Slack, Teams, or email for alerts +3. **Data Storage**: Connect to databases or data warehouses +4. **Custom Sources**: Extend beyond Hacker News to industry-specific sites + +### Advanced Features + +1. **Multi-Source Aggregation**: Reddit, Product Hunt, GitHub trending +2. **Sentiment Tracking**: Historical sentiment analysis and trending +3. **Competitor Monitoring**: Track specific companies or technologies +4. **AI Summarization**: Generate executive briefings and trend reports + +### Enterprise Integration + +1. **API Development**: RESTful API for integration with existing systems +2. **Dashboard Creation**: Real-time intelligence dashboards +3. **Workflow Automation**: Integration with marketing and research workflows +4. **Custom Analytics**: Business-specific KPIs and metrics + +## 🤝 Support and Professional Services + +This demo represents a starting point for enterprise automation workflows. For production implementation, custom development, or integration support: + +- **Technical Support**: Contact your Browserbase customer success team +- **Custom Development**: Professional services available for enterprise customization +- **Training**: Workshops and training sessions for your development team +- **Architecture Review**: Best practices consultation for large-scale deployments + +--- + +**Built with ❤️ using Browserbase and Stagehand** + +_This demo showcases the power of cloud browser automation for enterprise intelligence gathering. Ready to see how Browserbase can transform your web automation workflows?_ diff --git a/packages/examples/demos/hacker-news-intelligence/package.json b/packages/examples/demos/hacker-news-intelligence/package.json new file mode 100644 index 0000000000..96ef090f6f --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/package.json @@ -0,0 +1,34 @@ +{ + "name": "hacker-news-intelligence-demo", + "version": "1.0.0", + "description": "Enterprise-ready Browserbase demo for Hacker News content intelligence and automation", + "keywords": [ + "ai-automation", + "automation", + "browserbase", + "news-aggregation", + "stagehand", + "web-scraping" + ], + "license": "MIT", + "author": "Browserbase", + "main": "dist/main.js", + "scripts": { + "build": "tsc", + "start": "node dist/main.js", + "dev": "tsx src/main.ts", + "test": "tsx src/test.ts", + "clean": "rm -rf dist" + }, + "dependencies": { + "@browserbasehq/stagehand": "^1.5.0", + "chalk": "^5.3.0", + "dotenv": "^16.3.1", + "ora": "^8.0.1" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.6.0", + "typescript": "^5.3.0" + } +} diff --git a/packages/examples/demos/hacker-news-intelligence/src/config.ts b/packages/examples/demos/hacker-news-intelligence/src/config.ts new file mode 100644 index 0000000000..c839613252 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/config.ts @@ -0,0 +1,39 @@ +import { DemoConfig, BrowserbaseConfig } from "./types"; +import * as dotenv from "dotenv"; + +// Load environment variables +dotenv.config(); + +export const demoConfig: DemoConfig = { + maxPosts: parseInt(process.env.MAX_POSTS || "5"), + enableContentExtraction: process.env.ENABLE_CONTENT_EXTRACTION === "true", + timeoutMs: parseInt(process.env.TIMEOUT_MS || "30000"), + logLevel: (process.env.LOG_LEVEL as any) || "info", + outputFormat: "both", +}; + +export const browserbaseConfig: BrowserbaseConfig = { + apiKey: process.env.BROWSERBASE_API_KEY || "", + projectId: process.env.BROWSERBASE_PROJECT_ID || "", + region: "us-east-1", + proxies: true, + keepAlive: false, + fingerprint: { + screen: { width: 1920, height: 1080 }, + timezone: "America/New_York", + }, +}; + +export function validateConfig(): void { + if (!browserbaseConfig.apiKey) { + throw new Error("BROWSERBASE_API_KEY is required. Please check your .env file."); + } + + if (!browserbaseConfig.projectId) { + throw new Error("BROWSERBASE_PROJECT_ID is required. Please check your .env file."); + } + + if (demoConfig.maxPosts < 1 || demoConfig.maxPosts > 30) { + throw new Error("MAX_POSTS must be between 1 and 30"); + } +} diff --git a/packages/examples/demos/hacker-news-intelligence/src/extractor.ts b/packages/examples/demos/hacker-news-intelligence/src/extractor.ts new file mode 100644 index 0000000000..cf2af76aa4 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/extractor.ts @@ -0,0 +1,263 @@ +import { Stagehand } from "@browserbasehq/stagehand"; +import { HackerNewsPost, PostContent, PostAnalysis } from "./types"; +import { logger } from "./logger"; +import { demoConfig } from "./config"; + +export class HackerNewsExtractor { + private stagehand: Stagehand; + + constructor(stagehand: Stagehand) { + this.stagehand = stagehand; + } + + /** + * Extract top posts from Hacker News front page + */ + async extractTopPosts(): Promise { + logger.step(1, 4, "Navigating to Hacker News front page..."); + + await this.stagehand.page.goto("https://news.ycombinator.com"); + + logger.step(2, 4, `Extracting top ${demoConfig.maxPosts} posts...`); + + // Use Stagehand's AI-powered extraction + const postsData = await this.stagehand.page.extract( + `Extract the top ${demoConfig.maxPosts} posts from Hacker News front page. For each post, get: + - rank (position number) + - title + - URL (if it's an external link, get the actual URL; if it's a Hacker News discussion, note it) + - points (upvotes) + - author username + - comments count + - age text (how long ago it was posted) + - domain (if external) + Return as an array of objects with these exact field names.`, + ); + + logger.debug("Raw extracted data:", postsData); + + // Process and validate the extracted data + const posts = this.processExtractedPosts(postsData); + + logger.success(`Successfully extracted ${posts.length} posts`); + return posts; + } + + /** + * Extract content from individual post URLs + */ + async extractPostContent(post: HackerNewsPost): Promise { + if (!post.isExternal) { + logger.debug(`Skipping content extraction for Hacker News discussion: ${post.title}`); + return { + title: post.title, + url: post.url, + extractedText: "Hacker News discussion - no external content", + keyPoints: ["This is a Hacker News discussion post"], + wordCount: 0, + readingTimeMinutes: 0, + extractionSuccess: false, + error: "Internal Hacker News post", + }; + } + + try { + logger.debug(`Extracting content from: ${post.url}`); + + await this.stagehand.page.goto(post.url); + + // Use Stagehand's AI extraction for article content + const content = await this.stagehand.page.extract(` + Extract the main article content from this webpage. Return: + - title: The main article title + - extractedText: The full article text (clean, without ads or navigation) + - keyPoints: Array of 3-5 key points or main takeaways from the article + - wordCount: Approximate word count + Format as JSON with these exact field names. + `); + + const processedContent = this.processExtractedContent(content, post); + + logger.debug(`Content extracted for "${post.title}": ${processedContent.wordCount} words`); + return processedContent; + } catch (error) { + logger.error(`Failed to extract content from ${post.url}:`, error); + return { + title: post.title, + url: post.url, + extractedText: "", + keyPoints: [], + wordCount: 0, + readingTimeMinutes: 0, + extractionSuccess: false, + error: error instanceof Error ? error.message : "Unknown error", + }; + } + } + + /** + * Generate AI-powered analysis of extracted content + */ + async analyzePost(post: HackerNewsPost, content: PostContent): Promise { + try { + // Use Stagehand's AI capabilities for content analysis + const analysis = await this.stagehand.page.extract(` + Analyze this Hacker News post and its content: + + Post Title: ${post.title} + Domain: ${post.domain || "news.ycombinator.com"} + Points: ${post.points} + Comments: ${post.commentsCount} + Content: ${content.extractedText.substring(0, 1000)}... + + Provide analysis with: + - category: Tech category (e.g., "AI/ML", "Web Development", "Startup", "Hardware", "Security", "Other") + - sentiment: Overall sentiment (positive, neutral, negative) + - complexity: Technical complexity level (low, medium, high) + - businessRelevance: Business relevance score from 1-10 + + Return as JSON with these exact field names. + `); + + return { + post, + content, + analysis: this.processAnalysis(analysis), + }; + } catch (error) { + logger.warn(`Analysis failed for "${post.title}":`, error); + + // Fallback analysis + return { + post, + content, + analysis: { + category: "Other", + sentiment: "neutral", + complexity: "medium", + businessRelevance: 5, + }, + }; + } + } + + private processExtractedPosts(rawData: any): HackerNewsPost[] { + try { + logger.debug("Processing extracted data:", rawData); + + // Handle different possible response formats from Stagehand + let posts: any[] = []; + + if (Array.isArray(rawData)) { + posts = rawData; + } else if (rawData.extraction) { + // Stagehand returns data in extraction field + const extractionData = + typeof rawData.extraction === "string" + ? JSON.parse(rawData.extraction) + : rawData.extraction; + posts = Array.isArray(extractionData) ? extractionData : []; + } else if (rawData.posts) { + posts = rawData.posts; + } + + logger.debug(`Found ${posts.length} posts to process`); + + return posts.slice(0, demoConfig.maxPosts).map((item: any, index: number) => { + const url = item.URL || item.url || item.link || ""; + const isExternal = url && !url.includes("news.ycombinator.com") && url.startsWith("http"); + + // Extract numeric values from strings like "623 points" + const pointsStr = item.points || item["points"] || "0"; + const points = parseInt(pointsStr.toString().replace(/\D/g, "")) || 0; + + const commentsStr = item["comments count"] || item.comments || item.commentsCount || "0"; + const commentsCount = parseInt(commentsStr.toString().replace(/\D/g, "")) || 0; + + return { + rank: parseInt(item.rank?.toString().replace(/\D/g, "")) || index + 1, + title: item.title || "Unknown Title", + url: url, + points: points, + author: item["author username"] || item.author || item.user || "Unknown", + commentsCount: commentsCount, + ageText: item["age text"] || item.age || item.ageText || "Unknown", + domain: isExternal ? this.extractDomain(url) : undefined, + isExternal, + }; + }); + } catch (error) { + logger.error("Failed to process extracted posts:", error); + return []; + } + } + + private processExtractedContent(rawContent: any, post: HackerNewsPost): PostContent { + try { + const content = typeof rawContent === "string" ? JSON.parse(rawContent) : rawContent; + const extractedText = content.extractedText || content.text || ""; + const wordCount = this.countWords(extractedText); + + return { + title: content.title || post.title, + url: post.url, + extractedText, + keyPoints: Array.isArray(content.keyPoints) ? content.keyPoints : [], + wordCount, + readingTimeMinutes: Math.ceil(wordCount / 200), // Average reading speed + extractionSuccess: true, + }; + } catch (error) { + logger.error("Failed to process extracted content:", error); + return { + title: post.title, + url: post.url, + extractedText: "", + keyPoints: [], + wordCount: 0, + readingTimeMinutes: 0, + extractionSuccess: false, + error: "Content processing failed", + }; + } + } + + private processAnalysis(rawAnalysis: any): PostAnalysis["analysis"] { + try { + const analysis = typeof rawAnalysis === "string" ? JSON.parse(rawAnalysis) : rawAnalysis; + + return { + category: analysis.category || "Other", + sentiment: ["positive", "neutral", "negative"].includes(analysis.sentiment) + ? analysis.sentiment + : "neutral", + complexity: ["low", "medium", "high"].includes(analysis.complexity) + ? analysis.complexity + : "medium", + businessRelevance: Math.max(1, Math.min(10, parseInt(analysis.businessRelevance) || 5)), + }; + } catch (error) { + return { + category: "Other", + sentiment: "neutral", + complexity: "medium", + businessRelevance: 5, + }; + } + } + + private extractDomain(url: string): string { + try { + return new URL(url).hostname.replace("www.", ""); + } catch { + return "Unknown Domain"; + } + } + + private countWords(text: string): number { + return text + .trim() + .split(/\s+/) + .filter((word) => word.length > 0).length; + } +} diff --git a/packages/examples/demos/hacker-news-intelligence/src/logger.ts b/packages/examples/demos/hacker-news-intelligence/src/logger.ts new file mode 100644 index 0000000000..fced65a3a7 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/logger.ts @@ -0,0 +1,83 @@ +import chalk from "chalk"; +import { demoConfig } from "./config"; + +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, +} + +const logLevelMap = { + debug: LogLevel.DEBUG, + info: LogLevel.INFO, + warn: LogLevel.WARN, + error: LogLevel.ERROR, +}; + +class Logger { + private currentLevel: LogLevel; + + constructor() { + this.currentLevel = logLevelMap[demoConfig.logLevel] || LogLevel.INFO; + } + + private shouldLog(level: LogLevel): boolean { + return level >= this.currentLevel; + } + + private formatMessage(level: string, message: string, data?: any): string { + const timestamp = new Date().toISOString(); + const baseMessage = `[${timestamp}] [${level}] ${message}`; + + if (data) { + return `${baseMessage}\n${JSON.stringify(data, null, 2)}`; + } + + return baseMessage; + } + + debug(message: string, data?: any): void { + if (this.shouldLog(LogLevel.DEBUG)) { + console.log(chalk.gray(this.formatMessage("DEBUG", message, data))); + } + } + + info(message: string, data?: any): void { + if (this.shouldLog(LogLevel.INFO)) { + console.log(chalk.blue(this.formatMessage("INFO", message, data))); + } + } + + success(message: string, data?: any): void { + if (this.shouldLog(LogLevel.INFO)) { + console.log(chalk.green(this.formatMessage("SUCCESS", message, data))); + } + } + + warn(message: string, data?: any): void { + if (this.shouldLog(LogLevel.WARN)) { + console.warn(chalk.yellow(this.formatMessage("WARN", message, data))); + } + } + + error(message: string, error?: any): void { + if (this.shouldLog(LogLevel.ERROR)) { + const errorData = + error instanceof Error + ? { + message: error.message, + stack: error.stack, + } + : error; + console.error(chalk.red(this.formatMessage("ERROR", message, errorData))); + } + } + + step(stepNumber: number, totalSteps: number, message: string): void { + const progress = chalk.cyan(`[${stepNumber}/${totalSteps}]`); + console.log(`${progress} ${message}`); + } +} + +export const logger = new Logger(); diff --git a/packages/examples/demos/hacker-news-intelligence/src/main.ts b/packages/examples/demos/hacker-news-intelligence/src/main.ts new file mode 100644 index 0000000000..c0b00391dd --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/main.ts @@ -0,0 +1,222 @@ +#!/usr/bin/env node + +import { Stagehand } from "@browserbasehq/stagehand"; +import { HackerNewsExtractor } from "./extractor"; +import { IntelligenceReporter } from "./reporter"; +import { validateConfig, browserbaseConfig, demoConfig } from "./config"; +import { logger } from "./logger"; +import { PostAnalysis } from "./types"; +import chalk from "chalk"; +import ora from "ora"; + +/** + * Hacker News Intelligence Demo - Browserbase Enterprise Automation + * + * This demo showcases Browserbase's capabilities for: + * - AI-powered web scraping with Stagehand + * - Intelligent content extraction and analysis + * - Enterprise-grade error handling and reporting + * - Scalable news aggregation workflows + */ +class HackerNewsIntelligenceDemo { + private stagehand: Stagehand; + private extractor: HackerNewsExtractor; + private reporter: IntelligenceReporter; + + constructor() { + this.stagehand = new Stagehand({ + env: "BROWSERBASE", + apiKey: browserbaseConfig.apiKey, + projectId: browserbaseConfig.projectId, + ...browserbaseConfig, + }); + + this.extractor = new HackerNewsExtractor(this.stagehand); + this.reporter = new IntelligenceReporter(); + } + + /** + * Run the complete intelligence gathering workflow + */ + async run(): Promise { + const startTime = Date.now(); + let analyses: PostAnalysis[] = []; + + try { + // Initialize Stagehand + logger.info("🚀 Starting Hacker News Intelligence Demo"); + logger.info( + `Configuration: ${demoConfig.maxPosts} posts, content extraction: ${demoConfig.enableContentExtraction}`, + ); + + await this.stagehand.init(); + logger.success("Browserbase session initialized"); + + // Step 1: Extract top posts + const posts = await this.extractor.extractTopPosts(); + if (posts.length === 0) { + throw new Error("No posts extracted from Hacker News"); + } + + // Step 2: Process each post + logger.step(3, 4, `Processing ${posts.length} posts...`); + const spinner = ora("Analyzing posts...").start(); + + for (const [index, post] of posts.entries()) { + spinner.text = `Analyzing post ${index + 1}/${posts.length}: ${post.title}`; + + try { + // Extract content if enabled and post is external + let content; + if (demoConfig.enableContentExtraction) { + content = await this.extractor.extractPostContent(post); + } else { + content = { + title: post.title, + url: post.url, + extractedText: "Content extraction disabled", + keyPoints: [], + wordCount: 0, + readingTimeMinutes: 0, + extractionSuccess: false, + error: "Content extraction disabled in configuration", + }; + } + + // Generate AI analysis + const analysis = await this.extractor.analyzePost(post, content); + analyses.push(analysis); + + logger.debug(`Completed analysis for: ${post.title}`); + } catch (error) { + logger.error(`Failed to process post "${post.title}":`, error); + + // Add failed analysis to results + analyses.push({ + post, + content: { + title: post.title, + url: post.url, + extractedText: "", + keyPoints: [], + wordCount: 0, + readingTimeMinutes: 0, + extractionSuccess: false, + error: error instanceof Error ? error.message : "Processing failed", + }, + analysis: { + category: "Other", + sentiment: "neutral", + complexity: "medium", + businessRelevance: 1, + }, + }); + } + } + + spinner.succeed(`Completed analysis of ${analyses.length} posts`); + + // Step 3: Generate and display report + logger.step(4, 4, "Generating intelligence report..."); + const report = this.reporter.generateReport(analyses); + + // Display results + if (demoConfig.outputFormat === "console" || demoConfig.outputFormat === "both") { + this.reporter.displayConsoleReport(report); + } + + // Save JSON report + if (demoConfig.outputFormat === "json" || demoConfig.outputFormat === "both") { + await this.reporter.saveJsonReport(report); + } + + // Generate executive summary + const executiveSummary = this.reporter.generateExecutiveSummary(report); + logger.info("Executive Summary:", executiveSummary); + + // Performance metrics + const duration = (Date.now() - startTime) / 1000; + const successRate = (report.successfulExtractions / report.totalPostsAnalyzed) * 100; + + logger.success(`Demo completed successfully in ${duration.toFixed(1)}s`); + logger.info( + `Success rate: ${successRate.toFixed(1)}% (${report.successfulExtractions}/${report.totalPostsAnalyzed})`, + ); + } catch (error) { + logger.error("Demo execution failed:", error); + throw error; + } finally { + // Cleanup + try { + await this.stagehand.close(); + logger.debug("Browserbase session closed"); + } catch (error) { + logger.warn("Error closing Browserbase session:", error); + } + } + } + + /** + * Health check for demo dependencies + */ + async healthCheck(): Promise { + try { + logger.info("Running health check..."); + + validateConfig(); + logger.success("✅ Configuration validated"); + + // Test Browserbase connection + await this.stagehand.init(); + await this.stagehand.page.goto("https://httpbin.org/status/200"); + await this.stagehand.close(); + logger.success("✅ Browserbase connection verified"); + + logger.success("Health check passed - demo is ready to run"); + return true; + } catch (error) { + logger.error("Health check failed:", error); + return false; + } + } +} + +// CLI execution +async function main() { + const args = process.argv.slice(2); + const demo = new HackerNewsIntelligenceDemo(); + + try { + if (args.includes("--health-check")) { + const healthy = await demo.healthCheck(); + process.exit(healthy ? 0 : 1); + } else { + validateConfig(); + await demo.run(); + } + } catch (error) { + logger.error("Fatal error:", error); + console.log(chalk.red("\n❌ Demo failed to complete")); + console.log(chalk.yellow("💡 Try running with --health-check to verify setup")); + process.exit(1); + } +} + +// Handle graceful shutdown +process.on("SIGINT", () => { + logger.info("Received SIGINT, shutting down gracefully..."); + process.exit(0); +}); + +process.on("SIGTERM", () => { + logger.info("Received SIGTERM, shutting down gracefully..."); + process.exit(0); +}); + +// Export for programmatic use +export { HackerNewsIntelligenceDemo }; + +// Run if called directly +if (require.main === module) { + main(); +} diff --git a/packages/examples/demos/hacker-news-intelligence/src/reporter.ts b/packages/examples/demos/hacker-news-intelligence/src/reporter.ts new file mode 100644 index 0000000000..ffc25bb3a8 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/reporter.ts @@ -0,0 +1,245 @@ +import { IntelligenceReport, PostAnalysis } from "./types"; +import { logger } from "./logger"; +import { demoConfig } from "./config"; +import chalk from "chalk"; +import * as fs from "fs"; +import * as path from "path"; + +export class IntelligenceReporter { + /** + * Generate comprehensive intelligence report + */ + generateReport(analyses: PostAnalysis[]): IntelligenceReport { + const successful = analyses.filter((a) => a.content.extractionSuccess); + const totalComments = analyses.reduce((sum, a) => sum + a.post.commentsCount, 0); + const totalPoints = analyses.reduce((sum, a) => sum + a.post.points, 0); + + // Extract domains and count occurrences + const domainCounts: { [key: string]: number } = {}; + analyses.forEach((a) => { + if (a.post.domain) { + domainCounts[a.post.domain] = (domainCounts[a.post.domain] || 0) + 1; + } + }); + + const topDomains = Object.entries(domainCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 5) + .map(([domain]) => domain); + + // Extract key topics from titles and content + const keyTopics = this.extractKeyTopics(analyses); + + return { + generatedAt: new Date().toISOString(), + totalPostsAnalyzed: analyses.length, + successfulExtractions: successful.length, + posts: analyses, + summary: { + topDomains, + averagePoints: totalPoints / analyses.length, + totalComments, + keyTopics, + }, + }; + } + + /** + * Display report in console with rich formatting + */ + displayConsoleReport(report: IntelligenceReport): void { + console.log("\n"); + console.log(chalk.bold.cyan("🚀 HACKER NEWS INTELLIGENCE REPORT")); + console.log(chalk.gray("=".repeat(60))); + + // Summary + console.log(chalk.bold("\n📊 SUMMARY")); + console.log(`Generated: ${chalk.yellow(new Date(report.generatedAt).toLocaleString())}`); + console.log(`Posts Analyzed: ${chalk.green(report.totalPostsAnalyzed)}`); + console.log( + `Successful Extractions: ${chalk.green(report.successfulExtractions)}/${report.totalPostsAnalyzed}`, + ); + console.log(`Average Points: ${chalk.blue(Math.round(report.summary.averagePoints))}`); + console.log(`Total Comments: ${chalk.blue(report.summary.totalComments)}`); + + // Top Domains + if (report.summary.topDomains.length > 0) { + console.log(chalk.bold("\n🌐 TOP DOMAINS")); + report.summary.topDomains.forEach((domain, index) => { + console.log(`${index + 1}. ${chalk.cyan(domain)}`); + }); + } + + // Key Topics + if (report.summary.keyTopics.length > 0) { + console.log(chalk.bold("\n🏷️ KEY TOPICS")); + console.log(report.summary.keyTopics.map((topic) => chalk.magenta(`#${topic}`)).join(" ")); + } + + // Individual Posts + console.log(chalk.bold("\n📝 POST ANALYSIS")); + console.log(chalk.gray("-".repeat(60))); + + report.posts.forEach((analysis, index) => { + const { post, content, analysis: postAnalysis } = analysis; + + console.log(chalk.bold(`\n${index + 1}. ${post.title}`)); + console.log(` ${chalk.gray("URL:")} ${chalk.blue(post.url)}`); + console.log( + ` ${chalk.gray("Author:")} ${post.author} | ${chalk.gray("Points:")} ${post.points} | ${chalk.gray("Comments:")} ${post.commentsCount}`, + ); + console.log( + ` ${chalk.gray("Category:")} ${this.getCategoryEmoji(postAnalysis.category)} ${postAnalysis.category}`, + ); + console.log( + ` ${chalk.gray("Sentiment:")} ${this.getSentimentEmoji(postAnalysis.sentiment)} ${postAnalysis.sentiment}`, + ); + console.log( + ` ${chalk.gray("Complexity:")} ${this.getComplexityEmoji(postAnalysis.complexity)} ${postAnalysis.complexity}`, + ); + console.log( + ` ${chalk.gray("Business Relevance:")} ${"⭐".repeat(Math.round(postAnalysis.businessRelevance / 2))}`, + ); + + if (content.extractionSuccess && content.keyPoints.length > 0) { + console.log(` ${chalk.gray("Key Points:")}`); + content.keyPoints.slice(0, 3).forEach((point) => { + console.log(` • ${chalk.white(point)}`); + }); + console.log( + ` ${chalk.gray("Reading Time:")} ${content.readingTimeMinutes} min (${content.wordCount} words)`, + ); + } else if (content.error) { + console.log(` ${chalk.red("⚠️ Extraction failed:")} ${content.error}`); + } + }); + + console.log(chalk.gray("\n" + "=".repeat(60))); + console.log(chalk.green("✅ Report generation complete!\n")); + } + + /** + * Save report to JSON file + */ + async saveJsonReport(report: IntelligenceReport): Promise { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const filename = `hacker-news-report-${timestamp}.json`; + const filepath = path.join(process.cwd(), "reports", filename); + + // Ensure reports directory exists + const reportsDir = path.dirname(filepath); + if (!fs.existsSync(reportsDir)) { + fs.mkdirSync(reportsDir, { recursive: true }); + } + + // Save formatted JSON + const jsonContent = JSON.stringify(report, null, 2); + fs.writeFileSync(filepath, jsonContent, "utf8"); + + logger.success(`Report saved to: ${filepath}`); + return filepath; + } + + /** + * Generate executive summary for business stakeholders + */ + generateExecutiveSummary(report: IntelligenceReport): string { + const trending = report.posts.sort((a, b) => b.post.points - a.post.points).slice(0, 3); + + const highBusinessRelevance = report.posts.filter( + (p) => p.analysis.businessRelevance >= 8, + ).length; + + return ` +EXECUTIVE SUMMARY - Hacker News Intelligence Report +Generated: ${new Date(report.generatedAt).toLocaleDateString()} + +KEY INSIGHTS: +• Analyzed ${report.totalPostsAnalyzed} trending posts with ${report.summary.totalComments} total comments +• ${highBusinessRelevance} posts identified as high business relevance (8+ score) +• Average engagement: ${Math.round(report.summary.averagePoints)} points per post +• Top content domains: ${report.summary.topDomains.slice(0, 3).join(", ")} + +TRENDING TOPICS: +${trending.map((p, i) => `${i + 1}. ${p.post.title} (${p.post.points} points)`).join("\n")} + +RECOMMENDATIONS: +• Monitor posts with high business relevance scores for competitive intelligence +• Engage with trending topics in ${report.summary.keyTopics.slice(0, 2).join(" and ")} categories +• Consider content opportunities around emerging themes + `.trim(); + } + + private extractKeyTopics(analyses: PostAnalysis[]): string[] { + const topicCounts: { [key: string]: number } = {}; + + // Extract topics from categories + analyses.forEach((a) => { + const category = a.analysis.category.toLowerCase(); + if (category !== "other") { + topicCounts[category] = (topicCounts[category] || 0) + 1; + } + }); + + // Extract common keywords from titles + const commonWords = [ + "ai", + "ml", + "crypto", + "startup", + "open", + "source", + "web", + "security", + "data", + "tech", + ]; + analyses.forEach((a) => { + const title = a.post.title.toLowerCase(); + commonWords.forEach((word) => { + if (title.includes(word)) { + topicCounts[word] = (topicCounts[word] || 0) + 1; + } + }); + }); + + return Object.entries(topicCounts) + .sort(([, a], [, b]) => b - a) + .slice(0, 5) + .map(([topic]) => topic); + } + + private getCategoryEmoji(category: string): string { + const emojiMap: { [key: string]: string } = { + "AI/ML": "🤖", + "Web Development": "🌐", + Startup: "🚀", + Hardware: "💻", + Security: "🔒", + Crypto: "₿", + Mobile: "📱", + Data: "📊", + DevOps: "⚙️", + Other: "📄", + }; + return emojiMap[category] || "📄"; + } + + private getSentimentEmoji(sentiment: string): string { + const emojiMap = { + positive: "😊", + neutral: "😐", + negative: "😟", + }; + return emojiMap[sentiment as keyof typeof emojiMap] || "😐"; + } + + private getComplexityEmoji(complexity: string): string { + const emojiMap = { + low: "🟢", + medium: "🟡", + high: "🔴", + }; + return emojiMap[complexity as keyof typeof emojiMap] || "🟡"; + } +} diff --git a/packages/examples/demos/hacker-news-intelligence/src/test.ts b/packages/examples/demos/hacker-news-intelligence/src/test.ts new file mode 100644 index 0000000000..ddc6042b12 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/test.ts @@ -0,0 +1,114 @@ +#!/usr/bin/env node + +import { HackerNewsIntelligenceDemo } from "./main"; +import { logger } from "./logger"; +import chalk from "chalk"; + +/** + * Test suite for Hacker News Intelligence Demo + * Validates core functionality and Browserbase integration + */ +async function runTests() { + console.log(chalk.bold.blue("\n🧪 HACKER NEWS INTELLIGENCE DEMO - TEST SUITE")); + console.log(chalk.gray("=".repeat(60))); + + const demo = new HackerNewsIntelligenceDemo(); + let testsPassed = 0; + let totalTests = 0; + + const test = async (name: string, testFn: () => Promise) => { + totalTests++; + process.stdout.write(`${totalTests}. ${name}... `); + + try { + const result = await testFn(); + if (result) { + console.log(chalk.green("✅ PASS")); + testsPassed++; + } else { + console.log(chalk.red("❌ FAIL")); + } + } catch (error) { + console.log(chalk.red("❌ ERROR")); + logger.debug(`Test "${name}" failed:`, error); + } + }; + + // Test 1: Configuration validation + await test("Configuration validation", async () => { + try { + const { validateConfig } = await import("./config"); + validateConfig(); + return true; + } catch (error) { + return false; + } + }); + + // Test 2: Browserbase connection + await test("Browserbase connection", async () => { + return await demo.healthCheck(); + }); + + // Test 3: Basic extraction (limited to 2 posts for testing) + await test("Basic post extraction", async () => { + try { + // Temporarily override config for testing + const originalMaxPosts = process.env.MAX_POSTS; + process.env.MAX_POSTS = "2"; + process.env.ENABLE_CONTENT_EXTRACTION = "false"; + + const testDemo = new HackerNewsIntelligenceDemo(); + await testDemo.run(); + + // Restore original config + if (originalMaxPosts) { + process.env.MAX_POSTS = originalMaxPosts; + } + + return true; + } catch (error) { + logger.debug("Basic extraction test failed:", error); + return false; + } + }); + + // Test 4: Error handling + await test("Error handling", async () => { + try { + // Test with invalid configuration + const originalApiKey = process.env.BROWSERBASE_API_KEY; + process.env.BROWSERBASE_API_KEY = "invalid-key"; + + const testDemo = new HackerNewsIntelligenceDemo(); + const healthy = await testDemo.healthCheck(); + + // Restore original config + if (originalApiKey) { + process.env.BROWSERBASE_API_KEY = originalApiKey; + } + + // Should return false for invalid config + return !healthy; + } catch (error) { + return true; // Error handling working correctly + } + }); + + // Test Results + console.log(chalk.gray("\n" + "-".repeat(60))); + console.log(`Tests completed: ${testsPassed}/${totalTests} passed`); + + if (testsPassed === totalTests) { + console.log(chalk.green("🎉 All tests passed! Demo is ready for use.")); + process.exit(0); + } else { + console.log(chalk.red("⚠️ Some tests failed. Please check configuration and dependencies.")); + process.exit(1); + } +} + +// Run tests if called directly +if (require.main === module) { + runTests(); +} diff --git a/packages/examples/demos/hacker-news-intelligence/src/types.ts b/packages/examples/demos/hacker-news-intelligence/src/types.ts new file mode 100644 index 0000000000..4eb89c07b9 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/src/types.ts @@ -0,0 +1,70 @@ +/** + * Type definitions for Hacker News Intelligence Demo + */ + +export interface HackerNewsPost { + rank: number; + title: string; + url: string; + points: number; + author: string; + commentsCount: number; + ageText: string; + domain?: string; + isExternal: boolean; +} + +export interface PostContent { + title: string; + url: string; + extractedText: string; + keyPoints: string[]; + wordCount: number; + readingTimeMinutes: number; + extractionSuccess: boolean; + error?: string; +} + +export interface IntelligenceReport { + generatedAt: string; + totalPostsAnalyzed: number; + successfulExtractions: number; + posts: PostAnalysis[]; + summary: { + topDomains: string[]; + averagePoints: number; + totalComments: number; + keyTopics: string[]; + }; +} + +export interface PostAnalysis { + post: HackerNewsPost; + content: PostContent; + analysis: { + category: string; + sentiment: "positive" | "neutral" | "negative"; + complexity: "low" | "medium" | "high"; + businessRelevance: number; // 1-10 score + }; +} + +export interface DemoConfig { + maxPosts: number; + enableContentExtraction: boolean; + timeoutMs: number; + logLevel: "debug" | "info" | "warn" | "error"; + outputFormat: "json" | "console" | "both"; +} + +export interface BrowserbaseConfig { + apiKey: string; + projectId: string; + region?: string; + proxies?: boolean; + keepAlive?: boolean; + fingerprint?: { + screen?: { width: number; height: number }; + timezone?: string; + }; +} diff --git a/packages/examples/demos/hacker-news-intelligence/tsconfig.json b/packages/examples/demos/hacker-news-intelligence/tsconfig.json new file mode 100644 index 0000000000..1951778498 --- /dev/null +++ b/packages/examples/demos/hacker-news-intelligence/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/examples/demos/qa-agent/README.md b/packages/examples/demos/qa-agent/README.md new file mode 100644 index 0000000000..1bef1ed1c1 --- /dev/null +++ b/packages/examples/demos/qa-agent/README.md @@ -0,0 +1,228 @@ +# QA AI Agent Demo — Browserbase + Stagehand + +A demo showing how to build an AI-powered QA agent using [Browserbase](https://browserbase.com) and [Stagehand](https://stagehand.dev), with the [Vercel AI SDK](https://ai-sdk.dev) for tool orchestration. + +The demo includes a **buggy e-commerce app** with 20 intentional bugs and a **QA agent** that automatically finds them using two different architectural approaches. + +## Architecture + +``` +┌──────────────────────┐ +│ Vercel AI SDK │ Claude orchestrates the QA session +│ (Claude Sonnet) │ via tool calls +└──────────┬───────────┘ + │ tool calls + ▼ +┌──────────────────────┐ +│ Stagehand Tools │ act(), extract(), observe(), +│ (or Agent) │ screenshot(), a11y, UX audit +└──────────┬───────────┘ + │ browser commands + ▼ +┌──────────────────────┐ +│ Browserbase │ Cloud browser session +│ (Chrome) │ with session recording +└──────────┬───────────┘ + │ navigates + ▼ +┌──────────────────────┐ +│ BugMart App │ Next.js app with +│ (target URL) │ 20 intentional bugs +└──────────────────────┘ +``` + +## Two Approaches + +### Approach A: Primitives as Tools + +Each Stagehand primitive is exposed as an individual Vercel AI SDK tool. The LLM (Claude) decides which tool to call and when, maintaining full control over the testing flow. Includes programmatic checks like accessibility audits and a **UI/UX best practices audit** based on [userinterface.wiki](https://userinterface.wiki) (152 rules). + +**Tools:** `navigate`, `act`, `extract`, `observe`, `screenshot`, `get_console_logs`, `check_accessibility`, `check_ux_best_practices`, `get_page_url` + +### Approach B: Agent as Tool + +Stagehand's built-in `agent()` is wrapped as a single tool in hybrid mode (DOM + screenshot-based). The outer LLM acts as a coordinator, delegating high-level testing tasks to the agent which autonomously navigates and interacts. + +**Tools:** `stagehand_agent`, `extract_data`, `screenshot`, `get_console_logs` + +### Comparison + +| Dimension | Approach A (Primitives) | Approach B (Agent) | +| ------------------- | ---------------------------------------------- | -------------------------------------------------- | +| **Control** | Full — LLM decides every click | Delegated — agent navigates autonomously | +| **Custom checks** | Can run JS (console logs, a11y, UX audit) | Limited to what the agent sees visually | +| **Token usage** | Higher outer-loop (every step hits LLM) | Lower outer-loop, but agent uses tokens internally | +| **Debugging** | Every action visible as a tool call | Agent steps are partially opaque | +| **Code complexity** | More tool definitions to write | Fewer, simpler tools | +| **Flexibility** | Mix browser actions with custom logic | Constrained to agent capabilities | +| **Best for** | Protocol-driven testing with custom assertions | Exploratory testing, quick coverage | +| **Bug types found** | Visual + programmatic + UX violations | Primarily visual + functional | + +## Benchmark Results + +We ran both approaches against the same buggy app. Results are non-deterministic (LLM-based), but the pattern is consistent across runs: + +| Metric | Approach A (Primitives) | Approach B (Agent) | +| -------------- | ----------------------- | ------------------ | +| **Duration** | ~139s | ~337s | +| **Tool calls** | 34 | 15 (outer) | +| **Bugs found** | 11 | 6 | + +### What Each Approach Catches + +| Bug | Approach A | Approach B | +| --------------------------------- | ------------------ | ------------------ | +| Negative price (-$5.00) | :white_check_mark: | :white_check_mark: | +| Price 10x on detail pages | :white_check_mark: | :white_check_mark: | +| Broken image (keyboard 404) | :white_check_mark: | :x: | +| "Prodcuts" typo | :white_check_mark: | :x: | +| Missing alt text (a11y) | :white_check_mark: | :x: | +| Heading hierarchy skip (a11y) | :white_check_mark: | :x: | +| Console errors / 404s | :white_check_mark: | :white_check_mark: | +| Low contrast text (a11y) | :white_check_mark: | :x: | +| Small click targets (UX) | :white_check_mark: | :x: | +| Typography violations (UX) | :white_check_mark: | :x: | +| Disabled button no indicator (UX) | :white_check_mark: | :x: | +| Cart nav link → /carts (404) | :x: | :white_check_mark: | + +### Key Insights + +1. **Approach A excels at systematic, programmatic testing.** The `check_accessibility` and `check_ux_best_practices` tools run JavaScript audits that catch issues no amount of visual browsing would find (missing alt text, heading hierarchy, click target sizes, font-variant-numeric). + +2. **Approach B excels at exploratory, user-like testing.** The autonomous agent naturally clicks nav links and follows user flows, which is how it caught the broken cart link (`/carts` instead of `/cart`) that Approach A missed by navigating directly via URL. + +3. **Custom `page.evaluate()` tools don't mix well with the agent approach.** Adding UX audit tools to Approach B caused a regression (from 10 bugs to 5) because the coordinator had to waste steps navigating the agent back to pages just to run JS checks. Removing them restored performance. The lesson: **let each approach play to its strengths**. + +4. **Combine both for maximum coverage.** Run Approach A for protocol-driven testing with custom assertions, then run Approach B for exploratory testing that mimics real user behavior. Together they catch more than either alone. + +## Bug Catalog + +The buggy app contains 20 intentional bugs across different categories: + +| # | Type | Page | Description | Difficulty | +| --- | -------------- | -------------- | ------------------------------------------------- | ----------- | +| 01 | Missing image | Homepage | Product image returns 404 | Obvious | +| 02 | Wrong price | Product detail | Price displayed 10x too high | Non-obvious | +| 03 | Negative price | Homepage | Product shows -$5.00 | Obvious | +| 04 | Race condition | Cart | Double-click adds item twice | Non-obvious | +| 05 | Off-by-one | Cart | Quantity goes to 0 instead of removing item | Non-obvious | +| 06 | Typo | Homepage | Heading says "Our Prodcuts" | Obvious | +| 07 | Console error | All pages | Failed fetch to /api/analytics | Non-obvious | +| 08 | Dead button | Homepage | Add to Cart disabled on product #3, no visual cue | Non-obvious | +| 09 | Accessibility | Homepage | Images missing alt text | Non-obvious | +| 10 | Layout | Product detail | Description overlaps button on narrow viewport | Non-obvious | +| 11 | Calculation | Cart | Tax = subtotal x 0.8 instead of 0.08 (80% tax!) | Non-obvious | +| 12 | Calculation | Cart | Total doesn't include tax | Non-obvious | +| 13 | Validation | Checkout | Email field accepts any string | Non-obvious | +| 14 | Validation | Checkout | Card number accepts letters | Non-obvious | +| 15 | Dead button | Checkout | "Place Order" does nothing (TODO in code) | Obvious | +| 16 | Accessibility | About | Light gray text (#ccc) on white background | Non-obvious | +| 17 | Broken link | About | "Contact Us" links to /contact (404) | Obvious | +| 18 | Accessibility | About | Heading jumps h1 to h4 | Non-obvious | +| 19 | Broken link | Navbar | Cart link points to /carts (typo) | Obvious | +| 20 | CSS | Global | Modal z-index: -1 (behind content) | Non-obvious | + +## Setup + +### Prerequisites + +- Node.js 18+ +- A [Browserbase](https://browserbase.com) account +- An [Anthropic](https://console.anthropic.com) API key +- An [OpenAI](https://platform.openai.com) API key +- [ngrok](https://ngrok.com) (if running the buggy app locally — Browserbase needs a public URL) + +### 1. Start the Buggy App + +```bash +cd buggy-app +npm install +npm run dev +# Runs on http://localhost:3000 +``` + +If using Browserbase (cloud browser), expose your local app via ngrok: + +```bash +ngrok http 3000 +# Copy the https://xxxx.ngrok-free.app URL +``` + +### 2. Configure the QA Agent + +```bash +cd qa-agent +npm install +cp .env.example .env +``` + +Edit `.env` with your keys: + +``` +BROWSERBASE_API_KEY=your-key +BROWSERBASE_PROJECT_ID=your-project-id +OPENAI_API_KEY=your-key +ANTHROPIC_API_KEY=your-key +APP_URL=https://your-ngrok-url.ngrok-free.app +``` + +### 3. Run the QA Agent + +```bash +# Approach A: Primitives as Tools (with UX audit) +npm run approach-a + +# Approach B: Agent as Tool (exploratory) +npm run approach-b + +# Or use the CLI +npm start -- a +npm start -- b +``` + +Watch the live browser session in your [Browserbase dashboard](https://browserbase.com/sessions). + +## How It Works + +### Approach A Flow + +1. Claude receives a system prompt with the testing strategy +2. It calls `navigate` to go to each page +3. It uses `extract` to check data, `act` to interact, `observe` to plan +4. It runs `check_accessibility` and `check_ux_best_practices` for programmatic checks +5. It takes `screenshot`s to visually inspect pages +6. After testing all pages, it compiles a bug report + +### Approach B Flow + +1. Claude acts as a coordinator, breaking testing into chunks +2. Each chunk is delegated to `stagehand_agent` (e.g., "test the homepage for all visible issues") +3. The agent autonomously navigates, clicks, and explores in hybrid mode (DOM + screenshots) +4. Claude reviews agent results and extracts additional data +5. Console logs are checked between agent tasks +6. All findings are compiled into a final report + +## The UX Best Practices Tool + +Approach A includes a `check_ux_best_practices` tool that runs automated checks based on [userinterface.wiki](https://userinterface.wiki) — 152 rules across 12 categories. The tool runs `page.evaluate()` to programmatically check: + +- **Fitts's Law**: Interactive targets must be at least 32x32px +- **Hick's Law**: Navigation shouldn't have >7 items without grouping +- **Z-index hierarchy**: Flags negative z-index values +- **Tabular numbers**: Prices should use `font-variant-numeric: tabular-nums` +- **Text-wrap balance**: Headings should use `text-wrap: balance` +- **Active states**: Buttons should have transitions for feedback +- **Input types**: Email fields should use `type="email"`, not `type="text"` +- **Disabled states**: Disabled buttons must have visual indicators (opacity, cursor) +- **Progressive disclosure**: Forms with >6 fields should be broken into sections + +This tool is only effective in Approach A because it requires direct browser control via `page.evaluate()`. The agent in Approach B navigates autonomously and can't run custom JavaScript checks. + +## Extending This Demo + +- **Add more bugs**: Edit the app files in `buggy-app/src/` +- **Add more tools**: Extend `qa-agent/src/approach-a/tools.ts` +- **Add more UX rules**: Extend the `check_ux_best_practices` tool with rules from [userinterface.wiki](https://userinterface.wiki) +- **Custom test plans**: Modify the system prompts in the run files +- **Different models**: Change the model in `stagehand-init.ts` or the `generateText` calls +- **Combine approaches**: Run A then B for maximum coverage diff --git a/packages/examples/demos/qa-agent/buggy-app/next-env.d.ts b/packages/examples/demos/qa-agent/buggy-app/next-env.d.ts new file mode 100644 index 0000000000..40c3d68096 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/examples/demos/qa-agent/buggy-app/next.config.js b/packages/examples/demos/qa-agent/buggy-app/next.config.js new file mode 100644 index 0000000000..d918f80407 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/next.config.js @@ -0,0 +1,3 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; +module.exports = nextConfig; diff --git a/packages/examples/demos/qa-agent/buggy-app/package.json b/packages/examples/demos/qa-agent/buggy-app/package.json new file mode 100644 index 0000000000..9e4159ad26 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/package.json @@ -0,0 +1,24 @@ +{ + "name": "buggy-app", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.0" + } +} diff --git a/packages/examples/demos/qa-agent/buggy-app/postcss.config.js b/packages/examples/demos/qa-agent/buggy-app/postcss.config.js new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/about/page.tsx b/packages/examples/demos/qa-agent/buggy-app/src/app/about/page.tsx new file mode 100644 index 0000000000..eafb7a985c --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/about/page.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; + +export default function AboutPage() { + return ( +
+

About BugMart

+ + {/* BUG-18: Heading jumps from h1 to h4, skipping h2 and h3 */} +

Our Mission

+ + {/* BUG-16: Light gray text on white background — poor contrast */} +

+ BugMart was founded in 2024 with a simple mission: to provide high-quality electronics at + affordable prices. We believe that everyone deserves access to the best technology without + breaking the bank. Our team of experts carefully curates each product in our catalog to + ensure it meets our standards for quality, performance, and value. +

+ +

Our Team

+

+ We are a team of passionate tech enthusiasts who love helping people find the perfect + gadgets. With decades of combined experience in electronics retail, we know what makes a + great product. +

+ +

Get in Touch

+

+ Have questions or feedback? We would love to hear from you! +

+ + {/* BUG-17: Contact Us link goes to /contact which doesn't exist */} + + Contact Us + +
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/cart/page.tsx b/packages/examples/demos/qa-agent/buggy-app/src/app/cart/page.tsx new file mode 100644 index 0000000000..b7d81ff4c4 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/cart/page.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useCart } from "@/lib/cart-store"; +import CartSummary from "@/components/CartSummary"; +import Link from "next/link"; + +export default function CartPage() { + const { items, removeFromCart, updateQuantity } = useCart(); + + if (items.length === 0) { + return ( +
+

Your Cart is Empty

+

Add some products to get started!

+ + Continue Shopping + +
+ ); + } + + return ( +
+

Shopping Cart

+
+
+ {items.map((item) => ( +
+ +
+

{item.product.name}

+

${item.product.price.toFixed(2)} each

+
+
+ + {/* BUG-05 surfaces here: quantity can show 0 */} + {item.quantity} + +
+ + ${(item.product.price * item.quantity).toFixed(2)} + +
+ ))} +
+
+ + + Proceed to Checkout + +
+
+
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/checkout/page.tsx b/packages/examples/demos/qa-agent/buggy-app/src/app/checkout/page.tsx new file mode 100644 index 0000000000..e670d80171 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/checkout/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import CheckoutForm from "@/components/CheckoutForm"; +import CartSummary from "@/components/CartSummary"; + +export default function CheckoutPage() { + return ( +
+

Checkout

+
+
+ +
+
+ +
+
+
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/globals.css b/packages/examples/demos/qa-agent/buggy-app/src/app/globals.css new file mode 100644 index 0000000000..8ad35bb36f --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/globals.css @@ -0,0 +1,32 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: + system-ui, + -apple-system, + sans-serif; +} + +/* BUG-20: Modal z-index is -1, making modals appear behind content */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: -1; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-content { + background: white; + border-radius: 8px; + padding: 24px; + max-width: 500px; + width: 90%; +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/layout.tsx b/packages/examples/demos/qa-agent/buggy-app/src/app/layout.tsx new file mode 100644 index 0000000000..3b410af166 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import Navbar from "@/components/Navbar"; +import { CartProvider } from "@/lib/cart-store"; + +export const metadata: Metadata = { + title: "BugMart - Shop Electronics", + description: "Your favorite electronics store", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + +
{children}
+
+ + + ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/page.tsx b/packages/examples/demos/qa-agent/buggy-app/src/app/page.tsx new file mode 100644 index 0000000000..a998648976 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/page.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useEffect } from "react"; +import { products } from "@/lib/products"; +import ProductCard from "@/components/ProductCard"; + +export default function HomePage() { + // BUG-07: Console error — fetches non-existent analytics endpoint + useEffect(() => { + fetch("/api/analytics").catch(() => {}); + }, []); + + return ( +
+ {/* BUG-06: Typo — "Prodcuts" instead of "Products" */} +

Our Prodcuts

+
+ {products.map((product) => ( + + ))} +
+
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/app/product/[id]/page.tsx b/packages/examples/demos/qa-agent/buggy-app/src/app/product/[id]/page.tsx new file mode 100644 index 0000000000..f053eb2872 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/app/product/[id]/page.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { products } from "@/lib/products"; +import { useCart } from "@/lib/cart-store"; +import Link from "next/link"; + +export default function ProductPage() { + const params = useParams(); + const { addToCart } = useCart(); + const product = products.find((p) => p.id === Number(params.id)); + + if (!product) { + return ( +
+

Product Not Found

+ + Back to Home + +
+ ); + } + + return ( +
+
+
+ +
+
+ {product.category} +

{product.name}

+ + {/* BUG-10: Description has position absolute, overlaps button on narrow viewports */} +

+ {product.description} +

+ + {/* BUG-02: Price is multiplied by 10 (displayed as 10x actual) */} +
+ + ${(product.price * 10).toFixed(2)} + +
+ +
+ {product.inStock ? ( + + ) : ( + Out of Stock + )} + + Back to Shop + +
+
+
+
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/components/CartSummary.tsx b/packages/examples/demos/qa-agent/buggy-app/src/components/CartSummary.tsx new file mode 100644 index 0000000000..e6803c18e3 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/components/CartSummary.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useCart } from "@/lib/cart-store"; + +export default function CartSummary() { + const { items } = useCart(); + + const subtotal = items.reduce((sum, item) => sum + item.product.price * item.quantity, 0); + + // BUG-11: Tax is 80% instead of 8% (0.8 vs 0.08) + const tax = subtotal * 0.8; + + // BUG-12: Total doesn't include tax — just shows subtotal + const total = subtotal; + + return ( +
+

Order Summary

+
+
+ Subtotal + ${subtotal.toFixed(2)} +
+
+ Tax + ${tax.toFixed(2)} +
+
+ Total + ${total.toFixed(2)} +
+
+
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/components/CheckoutForm.tsx b/packages/examples/demos/qa-agent/buggy-app/src/components/CheckoutForm.tsx new file mode 100644 index 0000000000..d3a7d3af27 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/components/CheckoutForm.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useState } from "react"; + +export default function CheckoutForm() { + const [formData, setFormData] = useState({ + name: "", + email: "", + address: "", + city: "", + zip: "", + cardNumber: "", + expiry: "", + cvv: "", + }); + const [submitted, setSubmitted] = useState(false); + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + // BUG-15: Submit handler does nothing — TODO never implemented + // TODO: implement order submission + console.log("Form submitted", formData); + }; + + return ( +
+
+

Shipping Information

+
+
+ + +
+
+ + {/* BUG-13: Email field has type="text" — no email validation */} + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ +
+

Payment Information

+
+
+ + {/* BUG-14: Card number field accepts letters — no pattern/type restriction */} + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/components/Navbar.tsx b/packages/examples/demos/qa-agent/buggy-app/src/components/Navbar.tsx new file mode 100644 index 0000000000..f446b63952 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/components/Navbar.tsx @@ -0,0 +1,35 @@ +"use client"; + +import Link from "next/link"; +import { useCart } from "@/lib/cart-store"; + +export default function Navbar() { + const { totalItems } = useCart(); + + return ( + + ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/components/ProductCard.tsx b/packages/examples/demos/qa-agent/buggy-app/src/components/ProductCard.tsx new file mode 100644 index 0000000000..5dfc824e28 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/components/ProductCard.tsx @@ -0,0 +1,41 @@ +"use client"; + +import Link from "next/link"; +import { Product } from "@/lib/products"; +import { useCart } from "@/lib/cart-store"; + +export default function ProductCard({ product }: { product: Product }) { + const { addToCart } = useCart(); + + return ( +
+ + {/* BUG-09: Images have empty alt text — no descriptive alt */} + + +
+ +

+ {product.name} +

+ +

{product.description}

+
+ ${product.price.toFixed(2)} + {product.inStock ? ( + + ) : ( + Out of Stock + )} +
+
+
+ ); +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/lib/cart-store.ts b/packages/examples/demos/qa-agent/buggy-app/src/lib/cart-store.ts new file mode 100644 index 0000000000..461ee36454 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/lib/cart-store.ts @@ -0,0 +1,71 @@ +"use client"; + +import { createContext, useContext, useState, useCallback, ReactNode } from "react"; +import React from "react"; +import { Product } from "./products"; + +export interface CartItem { + product: Product; + quantity: number; +} + +interface CartContextType { + items: CartItem[]; + addToCart: (product: Product) => void; + removeFromCart: (productId: number) => void; + updateQuantity: (productId: number, quantity: number) => void; + clearCart: () => void; + totalItems: number; +} + +const CartContext = createContext(undefined); + +export function CartProvider({ children }: { children: ReactNode }) { + const [items, setItems] = useState([]); + + // BUG-04: Race condition — setTimeout causes double-click to add item twice + const addToCart = useCallback((product: Product) => { + setTimeout(() => { + setItems((prev) => { + const existing = prev.find((item) => item.product.id === product.id); + if (existing) { + return prev.map((item) => + item.product.id === product.id ? { ...item, quantity: item.quantity + 1 } : item, + ); + } + return [...prev, { product, quantity: 1 }]; + }); + }, 0); + }, []); + + // BUG-05: Off-by-one — decrements to 0 instead of removing + const removeFromCart = useCallback((productId: number) => { + setItems((prev) => + prev.map((item) => + item.product.id === productId ? { ...item, quantity: item.quantity - 1 } : item, + ), + ); + }, []); + + const updateQuantity = useCallback((productId: number, quantity: number) => { + setItems((prev) => + prev.map((item) => (item.product.id === productId ? { ...item, quantity } : item)), + ); + }, []); + + const clearCart = useCallback(() => setItems([]), []); + + const totalItems = items.reduce((sum, item) => sum + item.quantity, 0); + + return React.createElement( + CartContext.Provider, + { value: { items, addToCart, removeFromCart, updateQuantity, clearCart, totalItems } }, + children, + ); +} + +export function useCart() { + const context = useContext(CartContext); + if (!context) throw new Error("useCart must be used within CartProvider"); + return context; +} diff --git a/packages/examples/demos/qa-agent/buggy-app/src/lib/products.ts b/packages/examples/demos/qa-agent/buggy-app/src/lib/products.ts new file mode 100644 index 0000000000..412ed5e8a1 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/src/lib/products.ts @@ -0,0 +1,72 @@ +export interface Product { + id: number; + name: string; + description: string; + price: number; + image: string; + category: string; + inStock: boolean; +} + +export const products: Product[] = [ + { + id: 1, + name: "Wireless Bluetooth Speaker", + description: + "Portable speaker with 12-hour battery life and rich bass. Perfect for outdoor adventures and indoor gatherings alike.", + price: 49.99, + image: "https://picsum.photos/seed/speaker/400/400", + category: "Electronics", + inStock: true, + }, + { + id: 2, + name: "Premium Headphones", + description: + "Noise-cancelling over-ear headphones with premium sound quality. Features active noise cancellation and 30-hour battery life.", + price: 29.99, // BUG-02: Actually worth $299.90 - product detail page multiplies by 10 + image: "https://picsum.photos/seed/headphones/400/400", + category: "Electronics", + inStock: true, + }, + { + id: 3, + name: "Ergonomic Mouse", + description: + "Wireless ergonomic mouse designed for all-day comfort. Features adjustable DPI and silent clicks.", + price: 34.99, + image: "https://picsum.photos/seed/mouse/400/400", + category: "Accessories", + inStock: true, // BUG-08: Button is disabled despite being "in stock" + }, + { + id: 4, + name: "Mechanical Keyboard", + description: + "RGB mechanical keyboard with Cherry MX switches. Full-size layout with dedicated media controls.", + price: 89.99, + image: "/images/nonexistent-keyboard.png", // BUG-01: Image 404 + category: "Accessories", + inStock: true, + }, + { + id: 5, + name: "USB-C Hub", + description: + "7-in-1 USB-C hub with HDMI, USB 3.0, SD card reader, and power delivery passthrough.", + price: -5.0, // BUG-03: Negative price + image: "https://picsum.photos/seed/usbhub/400/400", + category: "Accessories", + inStock: true, + }, + { + id: 6, + name: "Laptop Stand", + description: + "Adjustable aluminum laptop stand with ventilation. Raises screen to eye level for better ergonomics.", + price: 45.99, + image: "https://picsum.photos/seed/stand/400/400", + category: "Accessories", + inStock: false, + }, +]; diff --git a/packages/examples/demos/qa-agent/buggy-app/tailwind.config.js b/packages/examples/demos/qa-agent/buggy-app/tailwind.config.js new file mode 100644 index 0000000000..629ad1197e --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/tailwind.config.js @@ -0,0 +1,6 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { extend: {} }, + plugins: [], +}; diff --git a/packages/examples/demos/qa-agent/buggy-app/tsconfig.json b/packages/examples/demos/qa-agent/buggy-app/tsconfig.json new file mode 100644 index 0000000000..49e4cf3b82 --- /dev/null +++ b/packages/examples/demos/qa-agent/buggy-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./src/*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/examples/demos/qa-agent/qa-agent/.env.example b/packages/examples/demos/qa-agent/qa-agent/.env.example new file mode 100644 index 0000000000..7c0e99d54a --- /dev/null +++ b/packages/examples/demos/qa-agent/qa-agent/.env.example @@ -0,0 +1,5 @@ +BROWSERBASE_API_KEY= +BROWSERBASE_PROJECT_ID= +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +APP_URL=http://localhost:3000 diff --git a/packages/examples/demos/qa-agent/qa-agent/package.json b/packages/examples/demos/qa-agent/qa-agent/package.json new file mode 100644 index 0000000000..74535952be --- /dev/null +++ b/packages/examples/demos/qa-agent/qa-agent/package.json @@ -0,0 +1,23 @@ +{ + "name": "qa-agent", + "version": "1.0.0", + "type": "module", + "scripts": { + "approach-a": "tsx src/approach-a/run.ts", + "approach-b": "tsx src/approach-b/run.ts", + "start": "tsx src/index.ts" + }, + "dependencies": { + "@ai-sdk/anthropic": "^1.0.0", + "@ai-sdk/openai": "^1.0.0", + "@browserbasehq/stagehand": "^2.5.2", + "ai": "^4.0.0", + "dotenv": "^16.4.5", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/packages/examples/demos/qa-agent/qa-agent/src/approach-a/run.ts b/packages/examples/demos/qa-agent/qa-agent/src/approach-a/run.ts new file mode 100644 index 0000000000..f44f55425d --- /dev/null +++ b/packages/examples/demos/qa-agent/qa-agent/src/approach-a/run.ts @@ -0,0 +1,92 @@ +import { generateText } from "ai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { createStagehand } from "../shared/stagehand-init.js"; +import { createTools } from "./tools.js"; +import "dotenv/config"; + +const BASE_URL = process.env.APP_URL || "http://localhost:3000"; + +const SYSTEM_PROMPT = `You are a senior QA engineer performing comprehensive testing on a web application. +The application is an e-commerce store called "BugMart" running at ${BASE_URL}. + +Your mission: Systematically test every page and feature, finding as many bugs as possible. + +## Testing Strategy + +Test each page in this order: +1. **Homepage** (${BASE_URL}/) - Check product listing, images, prices, text, buttons +2. **Product Detail Pages** (${BASE_URL}/product/1 through /product/6) - Check prices, descriptions, layout, add-to-cart +3. **Cart** (${BASE_URL}/cart) - Add items first, then check calculations, quantities, remove functionality +4. **Checkout** (${BASE_URL}/checkout) - Test form validation with invalid inputs, test submission +5. **About** (${BASE_URL}/about) - Check content, links, accessibility +6. **Navigation** - Test all nav links, check for broken links + +## What to Check on Each Page +- Visual: broken images, layout overlaps, typos in text +- Functional: buttons that don't work, forms that don't validate, broken links +- Data: incorrect prices, wrong calculations, negative values +- Console: JavaScript errors, failed network requests +- Accessibility: missing alt text, contrast issues, heading hierarchy, missing labels +- UX Best Practices: click target sizes, typography, spacing, z-index issues, disabled button indicators, form field types + +## Tools Available +- navigate: Go to a page +- act: Click buttons, fill forms, interact with elements +- extract: Pull data from the page to verify correctness +- observe: See what interactive elements are available +- screenshot: Take a screenshot for visual inspection +- get_console_logs: Check for JS errors +- check_accessibility: Run automated accessibility checks +- check_ux_best_practices: Run UI/UX best practices audit (userinterface.wiki 152 rules) +- get_page_url: Verify current URL + +## Bug Report Format +After testing all pages, output a comprehensive bug report with: +- Bug ID, title, severity (critical/major/minor/cosmetic) +- Page where it occurs +- Steps to reproduce +- Expected vs actual behavior + +Be thorough! A good QA engineer catches both obvious and subtle bugs.`; + +async function main() { + console.log("=".repeat(60)); + console.log("QA Agent - Approach A: Stagehand Primitives as Tools"); + console.log("=".repeat(60)); + console.log(`Target: ${BASE_URL}`); + console.log(); + + const stagehand = await createStagehand(); + const tools = createTools(stagehand); + const startTime = Date.now(); + + try { + const result = await generateText({ + model: anthropic("claude-sonnet-4-20250514"), + system: SYSTEM_PROMPT, + prompt: + "Begin your QA testing now. Start with the homepage and work through every page systematically. Use all available tools to find bugs. Report everything you find.", + tools, + maxSteps: 50, + }); + + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + + console.log("\n" + "=".repeat(60)); + console.log("QA REPORT - Approach A: Primitives as Tools"); + console.log("=".repeat(60)); + console.log(result.text); + console.log("\n" + "-".repeat(60)); + console.log(`Duration: ${duration}s`); + console.log(`Steps: ${result.steps?.length || "N/A"}`); + console.log( + `Tool calls: ${result.steps?.reduce((sum, s) => sum + (s.toolCalls?.length || 0), 0) || "N/A"}`, + ); + console.log("=".repeat(60)); + } finally { + await stagehand.close(); + console.log("\nSession closed."); + } +} + +main().catch(console.error); diff --git a/packages/examples/demos/qa-agent/qa-agent/src/approach-a/tools.ts b/packages/examples/demos/qa-agent/qa-agent/src/approach-a/tools.ts new file mode 100644 index 0000000000..90ab3ad0c6 --- /dev/null +++ b/packages/examples/demos/qa-agent/qa-agent/src/approach-a/tools.ts @@ -0,0 +1,486 @@ +import { tool } from "ai"; +import { z } from "zod"; +import { writeFileSync, mkdirSync } from "fs"; +import type { Stagehand } from "@browserbasehq/stagehand"; + +export function createTools(stagehand: Stagehand) { + const page = stagehand.page; + const consoleLogs: string[] = []; + + // Capture console messages (errors, warnings) + page.on("console", (msg) => { + const type = msg.type(); + if (type === "error" || type === "warning") { + consoleLogs.push(`[${type.toUpperCase()}] ${msg.text()}`); + } + }); + + // Capture page errors + page.on("pageerror", (err) => { + consoleLogs.push(`[PAGE_ERROR] ${err.message}`); + }); + + // Capture failed network requests + page.on("requestfailed", (req) => { + consoleLogs.push(`[NETWORK_ERROR] ${req.method()} ${req.url()} - ${req.failure()?.errorText}`); + }); + + return { + navigate: tool({ + description: + "Navigate to a URL in the browser. Use this to go to specific pages of the application.", + parameters: z.object({ + url: z.string().describe("The full URL to navigate to"), + }), + execute: async ({ url }) => { + try { + await page.goto(url, { waitUntil: "load", timeout: 60000 }); + } catch (error: any) { + // Even if timeout occurs, we may still be on the page + console.log(`Navigation warning: ${error.message?.substring(0, 100)}`); + } + try { + await page.waitForTimeout(2000); + const title = await page.title(); + const currentUrl = page.url(); + return { + success: true, + url: currentUrl, + title, + message: `Navigated to ${currentUrl} (title: "${title}")`, + }; + } catch (error: any) { + return { + success: false, + url, + error: error.message, + }; + } + }, + }), + + act: tool({ + description: + "Perform a browser action described in natural language. Examples: 'click the Add to Cart button', 'type hello@example.com into the email field', 'scroll down'. Use this for any interaction with the page.", + parameters: z.object({ + instruction: z.string().describe("Natural language description of the action to perform"), + }), + execute: async ({ instruction }) => { + try { + await stagehand.act(instruction); + return { success: true, action: instruction }; + } catch (error: any) { + return { + success: false, + action: instruction, + error: error.message, + }; + } + }, + }), + + extract: tool({ + description: + "Extract structured data from the current page using AI. Describe what information you want to extract. Returns the extracted data as a JSON object.", + parameters: z.object({ + instruction: z.string().describe("What data to extract from the current page"), + }), + execute: async ({ instruction }) => { + try { + const result = await stagehand.extract({ + instruction, + schema: z.object({ + data: z.any().describe("The extracted data"), + }), + }); + return { success: true, extracted: result }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }), + + observe: tool({ + description: + "Observe the current page and list available interactive elements and possible actions. Use this to understand what's on the page before acting.", + parameters: z.object({ + instruction: z + .string() + .describe( + "What to look for on the page, e.g. 'find all buttons' or 'find navigation links'", + ), + }), + execute: async ({ instruction }) => { + try { + const observations = await stagehand.observe(instruction); + return { success: true, observations }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }), + + screenshot: tool({ + description: + "Take a screenshot of the current page. Use this to visually inspect the page for layout issues, broken images, or other visual bugs.", + parameters: z.object({ + description: z + .string() + .optional() + .describe("Optional note about what to look for in the screenshot"), + }), + execute: async ({ description }) => { + try { + mkdirSync("screenshots", { recursive: true }); + const filename = `screenshots/screenshot-${Date.now()}.png`; + const buffer = await page.screenshot({ fullPage: true }); + writeFileSync(filename, buffer); + // Get page text content for analysis instead of sending huge base64 + const textContent = await page.evaluate(() => { + return document.body.innerText.substring(0, 3000); + }); + return { + success: true, + savedTo: filename, + note: description || "Screenshot captured", + url: page.url(), + pageText: textContent, + }; + } catch (error: any) { + return { success: false, error: error.message }; + } + }, + }), + + get_console_logs: tool({ + description: + "Get all JavaScript console errors and warnings captured since the session started. Use this to check for runtime errors, failed API calls, and other issues.", + parameters: z.object({}), + execute: async () => { + return { + logs: [...consoleLogs], + count: consoleLogs.length, + message: + consoleLogs.length > 0 + ? `Found ${consoleLogs.length} console issues` + : "No console errors detected", + }; + }, + }), + + check_accessibility: tool({ + description: + "Run accessibility checks on the current page. Checks for missing alt text, poor color contrast, missing form labels, heading hierarchy issues, and more.", + parameters: z.object({}), + execute: async () => { + const issues = await page.evaluate(() => { + const problems: string[] = []; + + // Check images without meaningful alt text + document.querySelectorAll("img").forEach((img, i) => { + if (!img.alt || img.alt.trim() === "") { + problems.push(`Image #${i + 1} missing alt text (src: ${img.src.substring(0, 80)})`); + } + }); + + // Check broken images + document.querySelectorAll("img").forEach((img, i) => { + if (!img.complete || img.naturalWidth === 0) { + problems.push(`Image #${i + 1} failed to load (src: ${img.src.substring(0, 80)})`); + } + }); + + // Check form inputs without labels + document.querySelectorAll("input, select, textarea").forEach((el) => { + const input = el as HTMLInputElement; + const id = input.id; + if (id && !document.querySelector(`label[for="${id}"]`)) { + problems.push(`Input "${id}" (type: ${input.type}) has no associated