LumaBrowser vs Puppeteer

Your existing Puppeteer scripts, running against a browser that understands what you meant when the selector breaks.

LumaBrowser embeds a Chrome DevTools Protocol server directly inside the browser process. Any Puppeteer, puppeteer-core, Playwright-over-CDP, or chrome-remote-interface client attaches the same way it attaches to a Chrome instance launched with --remote-debugging-port. Opt in to the Lumabyte.* CDP domain and brittle CSS selectors self-heal via an LLM when the DOM shifts underneath them.

The drop-in promise

LumaBrowser’s puppeteer-driver extension is a standards-compliant CDP WebSocket server on port 9222 — the same port and discovery surface Chromium exposes when launched with --remote-debugging-port=9222. The server implements Browser.*, Target.*, and the custom Lumabyte.* domain itself; every other command (Page, Runtime, DOM, Input, Network, Fetch, Emulation, Log, ~290 in total) forwards straight through Electron’s webContents.debugger to Chromium. What you don’t have to change:

  • No new library. Keep your existing puppeteer, puppeteer-core, Playwright, chrome-remote-interface, pyppeteer, or chromedp client.
  • The HTTP bootstrap surface matches Chromium. GET /json/version returns a webSocketDebuggerUrl. /json and /json/list enumerate targets. PUT /json/new?<url> opens a target. puppeteer.connect({ browserURL }) uses exactly these to discover the WebSocket endpoint.
  • Flat-mode sessions, modern default. Target.setAutoAttach with flatten: true is supported, matching modern Puppeteer’s wire format.
  • Browser contexts isolate state. Target.createBrowserContext returns a context ID that partitions cookies and storage (backed by a persistent persist:puppeteer-ctx-<id> partition).
  • Native CDP is untouched. Network.setUserAgentOverride, Fetch.enable, Emulation.setDeviceMetricsOverride, DOMSnapshot.captureSnapshot — all pass through to Chromium the same way they would against chrome --headless.

Just change the launch:

Puppeteer (classic)
// Downloads and launches bundled Chromium.
const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
  headless: 'new',
});

const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());

await browser.close();
LumaBrowser
// LumaBrowser is already running. Attach.
const puppeteer = require('puppeteer-core');

const browser = await puppeteer.connect({
  browserURL: 'http://127.0.0.1:9222',
});

const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());

await browser.disconnect();

That’s the entire migration. Swap puppeteer for puppeteer-core (no bundled Chromium), change launch(...) to connect({ browserURL }), point at port 9222. Everything below is what you gain by opting in.

What’s actually different

Three concrete wins. Each one is opt-in: pay the cost only where you want the benefit.

Win 1: Resilient selectors that survive a redesign

Scrapers break when a marketing team renames a CSS class. LumaBrowser’s Lumabyte.* CDP domain retries the find via an LLM using a natural-language description of what the element is — not how it’s currently styled.

Two opt-in modes:

  1. Keep your CSS, add a description fallback. Lumabyte.find({ description, selector }) tries the selector against DOM.querySelector first and only invokes the LLM when it misses. Happy-path scrapes cost nothing extra.
  2. Skip CSS entirely. Lumabyte.find({ description }) or Lumabyte.click({ description }) lets the LLM own resolution end-to-end.
Puppeteer (classic)
// Breaks the moment the button's class
// name changes during a UI refresh.
const puppeteer = require('puppeteer');

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.shop/cart');

await page.waitForSelector(
  'button.btn-primary.signup'
);
await page.click('button.btn-primary.signup');
LumaBrowser
// LLM fallback re-resolves when CSS misses.
const puppeteer = require('puppeteer-core');

const browser = await puppeteer.connect({
  browserURL: 'http://127.0.0.1:9222',
});
const page = await browser.newPage();
await page.goto('https://example.shop/cart');

const cdp = await page.target().createCDPSession();
await cdp.send('Lumabyte.configureFallback', {
  enabled: true,
});

// Option A: keep CSS, add a description fallback.
await cdp.send('Lumabyte.click', {
  description: 'the sign-up button',
  selector:    'button.btn-primary.signup',
});

// Option B: skip CSS entirely.
await cdp.send('Lumabyte.click', {
  description: 'the sign-up button',
});
Win 2: One round-trip to read the whole page

Classic Puppeteer agent loops make three separate CDP round-trips to capture page state: DOMSnapshot.captureSnapshot, then Accessibility.getFullAXTree, then Page.captureScreenshot. LumaBrowser’s Lumabyte.domSnapshot returns all three in a single call, ordered and correlated server-side.

Puppeteer (classic)
// Three CDP round-trips per agent step.
const cdp = await page.target().createCDPSession();
await cdp.send('DOM.enable');
await cdp.send('Accessibility.enable');

const snapshot = await cdp.send(
  'DOMSnapshot.captureSnapshot',
  { computedStyles: [] }
);
const axTree = await cdp.send(
  'Accessibility.getFullAXTree'
);
const { data: screenshot } = await cdp.send(
  'Page.captureScreenshot',
  { format: 'png' }
);
// 3 requests, 3 responses, 3 places to retry.
LumaBrowser
// One call, server-correlated payload.
const cdp = await page.target().createCDPSession();

const { snapshot, axTree, screenshot } =
  await cdp.send('Lumabyte.domSnapshot', {
    includeAxTree:     true,
    includeScreenshot: true,
  });

// snapshot   — DOMSnapshot.captureSnapshot
// axTree     — Accessibility.getFullAXTree
// screenshot — base64 PNG of the viewport
// 1 request. Parse locally.
Win 3: No bundled Chromium, dedicated automation tabs

Classic puppeteer ships a ~170 MB Chromium binary per platform and launches it as a child process for every script run. That binary drifts behind Chrome stable, your CI downloads it on every cold cache, and the --user-data-dir left behind needs cleanup. LumaBrowser’s CDP server runs inside the browser process itself — there is no binary to download, no launcher lifecycle, no temp profile to reap.

And unlike attaching to a user’s regular Chrome (where your automation would fight for the same tabs the user is browsing in), LumaBrowser gives the CDP client dedicated automation tabs: only tabs created with kind: 'puppeteer' surface as CDP targets, rendered in the tab strip with a purple accent and a PUP badge so you can watch the session drive them live.

Puppeteer (classic)
// puppeteer ships a bundled Chromium and
// spawns it as a child process on every run.
const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
  headless: 'new',
  args: ['--no-sandbox'],
  userDataDir: '/tmp/pup-profile',
});

// Plus: Chromium version drift, ~170MB
// per-platform binary, child-process cleanup,
// stale --user-data-dir trees, and the
// occasional "Failed to launch the browser
// process!" at 3am.
LumaBrowser
// LumaBrowser is already running. Attach.
const puppeteer = require('puppeteer-core');

const browser = await puppeteer.connect({
  browserURL: 'http://127.0.0.1:9222',
});

// Dedicated "PUP" tabs, visible in the strip.
// No binary download.
// No child process.
// No --user-data-dir to clean up.
// The user's regular tabs stay invisible to
// the CDP client, so Network Interceptor and
// AI Chat don't fight for the debugger lock.
Full example: a semantic end-to-end scrape

What a realistic flow looks like when you mix native CDP with Lumabyte.*. Notice how close the code reads to the task description.

const puppeteer = require('puppeteer-core');

// 1. Attach to the already-running browser.
const browser = await puppeteer.connect({
  browserURL: 'http://127.0.0.1:9222',
});
const page = await browser.newPage();
await page.goto('https://example.shop/products/coffee-grinder');

const cdp = await page.target().createCDPSession();

// 2. Opt in to LLM fallback for this session.
await cdp.send('Lumabyte.configureFallback', {
  enabled:            true,
  onFindFail:         true,   // retry failed finds via LLM
  onClickIntercepted: true,   // retry intercepted clicks via LLM
});

// 3. Describe elements, don't select them.
await cdp.send('Lumabyte.click', { description: 'the add-to-cart button' });
await cdp.send('Lumabyte.click', { description: 'the cart icon in the header' });

// 4. Pull DOM + AX tree + screenshot in one round-trip.
const { snapshot, axTree, screenshot } = await cdp.send('Lumabyte.domSnapshot', {
  includeAxTree:     true,
  includeScreenshot: true,
});

// 5. Native CDP still works — verify via Runtime.evaluate.
const { result } = await cdp.send('Runtime.evaluate', {
  expression:    'document.title',
  returnByValue: true,
});
console.log(result.value);

await browser.disconnect();

Twenty-odd lines, zero brittle CSS, one round-trip to capture the page state for your agent loop or visual diff. Try writing the same flow in classic Puppeteer and count the selectors you’d need to maintain next quarter.

Compatibility matrix

What works today against the puppeteer-driver extension, and where the additions sit:

FeatureStatusNotes
puppeteer.connect({ browserURL })SupportedDrop-in. Uses the standard Chrome bootstrap surface.
GET /json/version, /json, /json/listSupportedChrome-compatible DevTools target discovery.
PUT /json/new, /json/activate, /json/closeSupportedOpens, foregrounds, and closes puppeteer-kind tabs.
WebSocket root connection (/devtools/browser/{uuid})SupportedThe endpoint puppeteer.connect upgrades to.
Flat-mode sessions (Target.setAutoAttach flatten: true)SupportedMatches modern Puppeteer default.
Browser.* domainHandwrittenImplemented by the extension directly.
Target.* domainHandwrittenTarget discovery, auto-attach, browser contexts.
Page, Runtime, DOM, Input, Emulation, LogForwardedProxied through webContents.debugger to Chromium.
Network.* domain (setUserAgentOverride, emulateNetworkConditions, etc.)ForwardedFull native surface.
Fetch.* domain (request interception)ForwardedPause, modify, or fulfill any request.
DOMSnapshot.captureSnapshot, Accessibility.getFullAXTreeForwardedOr call Lumabyte.domSnapshot for both in one round-trip.
Browser contexts (Target.createBrowserContext)SupportedPersistent partition per context; cookies and storage isolated.
Lumabyte.find (description-first resolver)LumaBrowser onlyDeterministic-match → LLM → validation pipeline.
Lumabyte.click (semantic click)LumaBrowser onlyResolves, scrolls into view, dispatches synthetic input.
Lumabyte.domSnapshot (one-shot capture)LumaBrowser onlyDOM + optional AX tree + optional screenshot in one call.
Lumabyte.configureFallback / Lumabyte.getInfoLumaBrowser onlyPer-session LLM config, CDP’s analog of a WebDriver capability.
Automation targets whatever tab the user is onBy designOnly puppeteer-kind tabs surface as CDP targets. User tabs stay invisible.
puppeteer.launch() with bundled ChromiumNot applicableUse puppeteer-core + connect({ browserURL }); LumaBrowser is the browser.

If your workflow needs Network.* or Fetch.* request interception, it just works — those domains forward straight to Chromium. The Lumabyte.* additions are purely additive; existing Puppeteer code ignores them and keeps running.

How to enable it

Two ways, takes under a minute either way:

1. From the UI

  1. Open Settings → Puppeteer.
  2. (Optional) Check “Start automatically when LumaBrowser launches.”
  3. Click Save & Start.
  4. Point your Puppeteer client at http://127.0.0.1:9222.

2. Via the REST API

# Start the CDP server
curl -X POST http://localhost:3000/api/puppeteer/start

# Check status
curl http://localhost:3000/api/puppeteer/status

# Bind to all interfaces on a custom port
curl -X POST http://localhost:3000/api/puppeteer/settings \
  -H "Content-Type: application/json" \
  -d '{"host": "0.0.0.0", "port": 9333}'

For the full CDP domain reference, Lumabyte.* method signatures, and MCP tool surface, see the Puppeteer Driver section of the API docs.

FAQ

Does my existing Puppeteer script just work?

Yes. LumaBrowser implements the full CDP bootstrap surface (/json/version, /json/list, /json/new, /devtools/browser/{uuid}) and forwards every non-Lumabyte command straight through Electron’s webContents.debugger to Chromium. puppeteer.connect({ browserURL: 'http://127.0.0.1:9222' }) attaches the same way it does to any Chrome instance launched with --remote-debugging-port. Swap puppeteer for puppeteer-core (no bundled Chromium needed) and change launch() to connect({ browserURL }).

Can I use Playwright instead?

Yes. Playwright’s connect_over_cdp transport speaks the same protocol — point it at http://127.0.0.1:9222 and it attaches exactly like puppeteer.connect. Any CDP client works: chrome-remote-interface in Node, pyppeteer in Python, chromedp in Go, anything that speaks CDP over WebSocket. The Lumabyte.* domain is available from all of them via the generic send(method, params) entry point.

Which LLM runs the Lumabyte.find fallback?

The fallback routes through whichever model you configure in the shared core-scope selector-resolver slot. Configure it once and every description-based call from either the Puppeteer or the Selenium driver benefits — same orchestrator, same pipeline. If the slot is unconfigured, the resolver falls back to your global active LLM. You can point it at Anthropic, any OpenAI-compatible endpoint, or a local model via LM Studio or Ollama.

Is it as fast as launching my own Chromium?

Faster to start, identical in steady state. There’s no Chromium download, no child-process launch, and no --user-data-dir cleanup — you’re attaching to a browser that’s already running. Individual CDP commands run through webContents.debugger in-process with Chromium, so there’s no IPC overhead vs a native Puppeteer-launched instance. The LLM fallback adds latency only when the primary selector misses and fallback is enabled for that session.

Will Puppeteer see my regular browsing tabs?

No, and that’s deliberate. Only tabs created with kind: 'puppeteer' (via browser.newPage() or PUT /json/new) surface as CDP targets. They render in the tab strip with a purple accent and a PUP badge so you can watch the automation drive them. Your regular tabs stay invisible to the CDP client, so LumaBrowser’s Network Interceptor, AI Chat browsing tools, and other subsystems don’t fight Puppeteer for the exclusive webContents.debugger lock.

Does request interception (Fetch domain) work?

Yes. Fetch.enable, Fetch.requestPaused, Fetch.continueRequest, Fetch.fulfillRequest, and Fetch.failRequest all forward to Chromium unchanged. The same goes for Network.setRequestInterception and the legacy interception path. If you’ve been using CDP passthrough tricks under Selenium to get request interception, switching to the Puppeteer driver is the direct path.

What about Selenium or WebdriverIO?

LumaBrowser ships a separate WebDriver server on port 9515 alongside this one. Your Selenium, WebdriverIO, Capybara, or Nightwatch suite connects unchanged with the same LLM fallback exposed as the ai-description locator strategy. See the LumaBrowser vs Selenium page and the Puppeteer vs Selenium comparison for when to pick each driver.

Ready to try it

Install LumaBrowser, enable the puppeteer-driver extension, and swap puppeteer.launch() for puppeteer.connect({ browserURL: 'http://127.0.0.1:9222' }). Every native CDP command keeps working. The Lumabyte.* domain is there when you want it.