Ego-Browser Data Flow Architecture: From Heredoc to Chrome DevTools Protocol

Ego-browser streams a JavaScript heredoc through four architectural layers—CLI ingestion, script compilation with helper injection, facade-to-CDP routing, and ego-bridge message passing—to execute automation commands in a live browser.

The ego-browser CLI in the citrolabs/ego-lite repository transforms inline shell heredocs into full browser automation sessions. Understanding this data flow reveals how a simple await page.goto() call travels from stdin through Node.js facades and finally reaches the Chrome DevTools Protocol (CDP) runtime.

CLI Entry Point: Reading the Heredoc from Stdin

The journey begins in package/ego-browser/src/index.ts. When invoked directly from the shell, the executable detects CLI mode and reads the complete heredoc text from stdin.

// Excerpt from src/index.ts (lines 56-64)
const code = await readStdin();
await runMain(code, { flushSink: true });

The runMain function—imported from src/run.ts—receives the raw JavaScript string and prepares it for execution. This layer handles argument parsing, SDK installation via installEgoSdk, and coordinates the output lifecycle. All console output is captured rather than streamed immediately; it flows into a bufferOutput sink that flushes only upon successful completion or discards on hard-stop (lines 75-82 in index.ts).

Script Compilation and Helper Context Injection

Once the heredoc text is captured, runMain in src/run.ts constructs an executable wrapper around the user code.

// From src/run.ts (lines 11-18)
const names = Object.keys(context);
const fn = new AsyncFunction(...names, `"use strict"; ${code}`);
const result = await fn(...Object.values(context));

The context object is built by helpers.helperContext() from src/helpers.ts. This injection supplies four critical facades:

  • page — Playwright-style navigation and element interaction
  • browser — Tab and context management
  • taskSpaces — Isolated execution environments
  • fetch — Network request interception

By wrapping the heredoc in an AsyncFunction with these pre-bound names, user scripts execute with full async/await support and immediate access to automation primitives without explicit imports.

Facade-to-Driver-to-CDP: The Command Routing Layer

When a heredoc script calls await page.goto(url), the call traverses three internal abstractions before reaching the browser.

1. Page Facade (createPageFacade)

// Simplified from src/helpers.ts
function createPageFacade() {
  return {
    goto: nav.goto,
    title: async () => (await nav.pageInfo()).title,
    locator: createLocator,
    // ... additional methods
  };
}

2. Navigation Driver (src/driver/nav.ts)

The facade delegates to concrete drivers. The navigation driver translates high-level commands into CDP method calls:

// From src/driver/nav.ts
export async function goto(url: string, options = {}) {
  await cdp('Page.navigate', { url, referrer: options.referrer });
  // wait for load state, handle lifecycle events
}

3. CDP Wrapper (src/browser-runtime.ts)

The cdp function imported by drivers is actually browserCdp, which manages session state and message serialization:

// From src/browser-runtime.ts (lines 31-45)
export async function browserCdp(method, params = {}, sessionId) {
  if (!sessionId && !BROWSER_LEVEL(method)) {
    sessionId = await ensureSession();      // creates or refreshes CDP session
  }
  return rawCdp(method, params, sessionId); // dispatches to ego bridge
}

The ensureSession() call guarantees an active CDP session before any domain-specific command executes. Session management is transparent to user scripts but essential for multiplexing multiple tabs or contexts.

Ego Bridge: Native Runtime to Chrome DevTools Protocol

The final layer crosses from JavaScript into the native ego-lite runtime. The rawCdp function invokes ego.sendCDPMessage, a method provided by the host binary:

// Conceptual flow from src/browser-runtime.ts
const response = await ego.sendCDPMessage({
  sessionId,
  method,
  params
});

The ego object exists only within the ego-lite runtime environment. Its sendCDPMessage implementation:

  1. Serializes the message to JSON
  2. Transmits via the native bridge to the attached browser's CDP endpoint
  3. Awaits the response from Chrome
  4. Returns the result through handleMessage (lines 39-50 in browser-runtime.ts)

Response correlation happens through promise resolution. Each outgoing CDP request registers a pending promise keyed by message ID; when handleMessage receives the matching response, it resolves the original caller's promise, unwinding the stack back through driver, facade, and finally to the awaiting heredoc script.

Output Handling and Sink Flushing

Console output from the heredoc does not write directly to stdout. Instead, runMain and installEgoSdk intercept all console.log calls and redirect to bufferOutput. This buffered sink serves two purposes: it prevents interleaving with CDP debug traffic, and it enables atomic output—either the complete successful result or nothing on crash.

// From src/run.ts (lines 40-46)
function bufferOutput() {
  const chunks: string[] = [];
  return {
    write: (s: string) => chunks.push(s),
    flush: () => process.stdout.write(chunks.join('')),
    discard: () => { chunks.length = 0; }
  };
}

When runMain completes with { flushSink: true }, the accumulated chunks are written to process.stdout in a single operation.

Complete Data Flow Example

A minimal heredoc demonstrates the full pipeline:

ego-browser <<'JS'
await page.goto('https://example.com')
console.log('Title →', await page.title())
JS
Step Layer Action
1 CLI index.ts reads heredoc, calls runMain(code)
2 Compiler run.ts wraps code in AsyncFunction, injects page from helperContext()
3 Facade page.gotonav.goto in driver
4 CDP Wrapper browserCdp('Page.navigate', ...)ensureSession()rawCdp()
5 Ego Bridge ego.sendCDPMessage transmits to Chrome
6 Browser Chrome navigates, returns frameId
7 Response handleMessage resolves promise, result bubbles to script
8 Evaluation page.title() triggers Runtime.evaluate via same path
9 Output console.log buffers Title → Example Domain
10 Flush Sink writes complete output to stdout

Summary

  • Heredoc ingestion: src/index.ts captures stdin and delegates to runMain in src/run.ts
  • Context injection: src/helpers.ts builds the page/browser/taskSpaces/fetch facades
  • Command routing: Facades → drivers (src/driver/*) → browserCdprawCdp
  • Session management: ensureSession() in src/browser-runtime.ts maintains CDP session lifecycle
  • Native bridge: ego.sendCDPMessage transmits to Chrome; handleMessage routes responses
  • Output control: Buffered sink in run.ts enables atomic stdout writes

Frequently Asked Questions

What file handles the initial heredoc reading in ego-browser?

The CLI entry point package/ego-browser/src/index.ts detects direct invocation, reads the complete stdin buffer containing the heredoc, and passes it to runMain. Lines 56-64 implement this bootstrap logic.

How does ego-browser maintain Chrome DevTools Protocol sessions?

The browserCdp function in src/browser-runtime.ts automatically invokes ensureSession() before any non-browser-level CDP command. This creates or refreshes a session ID that persists across multiple calls from the same script execution.

Why is console output buffered rather than streamed immediately?

The bufferOutput sink in src/run.ts (lines 40-46) captures all console output to prevent corruption from interleaved CDP debug messages and to support atomic output semantics—either the complete successful result is printed, or nothing if the script crashes or is terminated.

Can I use require/import in an ego-browser heredoc?

No. The heredoc executes as a bare AsyncFunction with injected globals—there is no module system. All required functionality is pre-bound via helperContext() from src/helpers.ts, providing page, browser, taskSpaces, and fetch without explicit imports.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →