← back to Abrams Report

scrapers/fetch-browserbase.js

108 lines

'use strict';
// Cloud Chromium fetch via Browserbase — bypasses Cloudflare / anti-bot for sites like Wallquest.
// Credentials live in ~/.claude/skills/browserbase/.env on Mac2 and /root/.env on Kamatera.

const path = require('path');
const fs = require('fs');
const os = require('os');

// Try local skill env first, then ~/.env, then process.env
function loadCreds() {
  if (process.env.BROWSERBASE_API_KEY && process.env.BROWSERBASE_PROJECT_ID) {
    return {
      apiKey: process.env.BROWSERBASE_API_KEY,
      projectId: process.env.BROWSERBASE_PROJECT_ID,
    };
  }
  const candidates = [
    // Project-local .env (preferred — populated by secrets-manager)
    path.join(__dirname, '..', '.env'),
    path.join(os.homedir(), '.claude/skills/browserbase/.env'),
    path.join(os.homedir(), '.env'),
    '/root/.env',
  ];
  for (const f of candidates) {
    try {
      const txt = fs.readFileSync(f, 'utf8');
      const obj = {};
      txt.split('\n').forEach(line => {
        const m = line.match(/^([A-Z_]+)=(.+)$/);
        if (m) obj[m[1]] = m[2].replace(/^["']|["']$/g, '');
      });
      if (obj.BROWSERBASE_API_KEY && obj.BROWSERBASE_PROJECT_ID) {
        return { apiKey: obj.BROWSERBASE_API_KEY, projectId: obj.BROWSERBASE_PROJECT_ID };
      }
    } catch {}
  }
  return null;
}

async function fetchViaBrowserbase(url) {
  const creds = loadCreds();
  if (!creds) throw new Error('Browserbase creds not found in env or .env files');

  // Lazy-load to avoid pulling chromium when not needed
  let Browserbase, chromium;
  try {
    Browserbase = require('@browserbasehq/sdk').default || require('@browserbasehq/sdk').Browserbase;
    chromium = require('playwright-core').chromium;
  } catch (e) {
    // Fallback: load from the browserbase skill's node_modules
    const skillNm = path.join(os.homedir(), '.claude/skills/browserbase/node_modules');
    try {
      Browserbase = require(path.join(skillNm, '@browserbasehq/sdk')).default;
      chromium = require(path.join(skillNm, 'playwright-core')).chromium;
    } catch (e2) {
      throw new Error('Browserbase SDK not installed: ' + e.message);
    }
  }

  const bb = new Browserbase({ apiKey: creds.apiKey });

  let trackedEnd = () => {};
  try {
    const { trackedSession } = require(path.join(os.homedir(), '.claude/skills/browserbase/track-session.js'));
    const wrapped = await trackedSession(bb, creds.projectId, { app: 'abrams-report', note: url.slice(0, 120) });
    var session = wrapped.session;
    trackedEnd = wrapped.end;
  } catch {
    // helper missing — fall back to raw session creation (no cost log)
    var session = await bb.sessions.create({ projectId: creds.projectId });
  }
  const browser = await chromium.connectOverCDP(session.connectUrl);

  try {
    const ctx = browser.contexts()[0] || (await browser.newContext());
    const page = ctx.pages()[0] || (await ctx.newPage());

    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 35000 });
    // Give Cloudflare a moment to clear the challenge if any
    await page.waitForTimeout(2500);

    // If this is an XML/sitemap URL, Chrome wraps it in pretty-print HTML.
    // Try fetching the raw resource through the page context (bypasses the wrapper
    // and preserves the same-site cookies set by the initial navigation).
    if (/\.xml(\?|$)|sitemap/i.test(url)) {
      try {
        const xml = await page.evaluate(async (u) => {
          const r = await fetch(u, {
            headers: { Accept: 'application/xml, text/xml, */*' },
          });
          return await r.text();
        }, url);
        if (xml && xml.length > 200 && xml.trim().startsWith('<')) {
          return xml;
        }
      } catch {}
    }

    const html = await page.content();
    return html;
  } finally {
    try { await browser.close(); } catch {}
    try { await trackedEnd(); } catch {}
  }
}

module.exports = { fetchViaBrowserbase };