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

> Explore the ego-browser data flow architecture. Learn how JavaScript heredoc commands travel from CLI ingestion to Chrome DevTools Protocol execution in a live browser.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: architecture
- Published: 2026-08-29

---

**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`](https://github.com/citrolabs/ego-lite/blob/main/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**.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/index.ts)).

## Script Compilation and Helper Context Injection

Once the heredoc text is captured, `runMain` in [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) constructs an executable wrapper around the user code.

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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`)**

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/driver/nav.ts))**

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

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/src/browser-runtime.ts))**

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

```typescript
// 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:

```typescript
// 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`](https://github.com/citrolabs/ego-lite/blob/main/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.

```typescript
// 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:

```bash
ego-browser <<'JS'
await page.goto('https://example.com')
console.log('Title →', await page.title())
JS

```

| Step | Layer | Action |
|------|-------|--------|
| 1 | CLI | [`index.ts`](https://github.com/citrolabs/ego-lite/blob/main/index.ts) reads heredoc, calls `runMain(code)` |
| 2 | Compiler | [`run.ts`](https://github.com/citrolabs/ego-lite/blob/main/run.ts) wraps code in `AsyncFunction`, injects `page` from `helperContext()` |
| 3 | Facade | `page.goto` → `nav.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`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts) captures stdin and delegates to `runMain` in [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts)
- **Context injection**: [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) builds the `page`/`browser`/`taskSpaces`/`fetch` facades
- **Command routing**: Facades → drivers (`src/driver/*`) → `browserCdp` → `rawCdp`
- **Session management**: `ensureSession()` in [`src/browser-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/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`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts), providing `page`, `browser`, `taskSpaces`, and `fetch` without explicit imports.