How stdin JavaScript Becomes Browser Commands in ego-lite: Complete Data Flow Explained

stdin JavaScript in ego-lite flows through a 7-stage pipeline—entry point wrapping, helper context injection, driver layer transformation, CDP payload construction, host bridge transmission, event queue handling, and ref-map resolution—to execute as Chrome DevTools Protocol commands against the embedded browser.

The citrolabs/ego-lite repository implements a TypeScript runtime that transforms ordinary JavaScript scripts into browser automation commands without spawning external processes. This article traces the complete data flow from the moment code enters through stdin to when CDP responses return to your script.

Entry Point: Receiving and Wrapping stdin Input

The data flow begins in package/ego-browser/src/index.ts, where the CLI consumes the entire stdin stream and dispatches execution through one of two paths.

When invoked as a CLI tool, runMain() receives the raw script string, wraps it in an async function, injects helper bindings, and passes the result to the execution layer. When imported as a module, installEgoSdk() performs equivalent initialization for programmatic use.

// From src/index.ts - conceptual flow
runMain(stdinScript) → wraps in async IIFE → injects helpers → calls run()

The wrapping process ensures top-level await works correctly and establishes the lexical scope where helper references (nav, pointer, js, etc.) resolve to the injected context.

Script Execution: Building the Helper Context

The run() function in package/ego-browser/src/run.ts orchestrates script execution. It creates a fresh helper context by calling helperContext() from src/helpers.ts.

This context object contains every public automation primitive:

  • nav — navigation operations (goto, reload, back, forward)
  • pointer — mouse interactions (click, move, drag)
  • keyboard — key input and combinations
  • js — JavaScript evaluation in page context
  • $ — element locator and ref resolution
  • observe — snapshot and state inspection

The user script executes with this context as its lexical environment, so direct calls like await nav.goto(url) immediately invoke the bound helper methods.

Driver Layer: Transforming Helpers to CDP Semantics

Each helper delegates to domain-specific drivers under package/ego-browser/src/driver/. These drivers translate high-level intentions into CDP method calls.

Navigation (src/driver/nav.ts):

// User writes:
await nav.goto('https://example.com');

// Driver builds:
{ method: 'Page.navigate', params: { url: 'https://example.com' }}

Pointer (src/driver/pointer.ts):

// User writes:
await pointer.click(100, 200);

// Driver builds:
{ method: 'Input.dispatchMouseEvent', 
  params: { type: 'mousePressed', x: 100, y: 200, ... }}

Drivers also handle retry logic and error classification. Failures wrap in ElementResolutionError with a transient boolean flag, enabling automatic retry by surrounding waits infrastructure.

Runtime Bridge: From JavaScript Objects to CDP Transport

All driver outputs converge on browserRuntime() in package/ego-browser/src/browser-runtime.ts. This singleton manages:

  • The active CDP session reference
  • A 10,000-capacity event queue for buffering responses
  • Pending command promises awaiting resolution

The runtime constructs final CDP request objects and forwards them through ego.sendCDPMessage—the sole boundary to the closed-source host environment that manages the actual browser instance.

Low-Level CDP Construction: cdp-eval.ts

For operations requiring direct protocol access, package/ego-browser/src/cdp-eval.ts provides two primitives:

  • cdp(method, params) — sends any raw CDP method with arbitrary parameters
  • js(expression) — evaluates JavaScript in page context, automatically wrapping expressions lacking explicit return in an IIFE
// From cdp-eval.ts usage patterns
await cdp('Runtime.evaluate', { expression: 'document.title' });

await js`document.title`;  // Equivalent, with automatic IIFE wrapping

The js tagged template literal preserves source location information while handling the return-value extraction automatically.

Ref Resolution and Snapshot Lifecycle

After navigation or DOM-mutation operations, the runtime triggers snapshot capture in package/ego-browser/src/ref-map.ts. This rebuilds the ref-map that associates numeric refs (e.g., @21) with backend node IDs.

When user scripts reference refs:

const element = await $`@12`;  // Resolves numeric ref via ref-map

package/ego-browser/src/element-resolver.ts performs the lookup. If the ref-map is stale, it automatically re-snapshots before resolution. The resolver also classifies errors: transient failures (network, timing) trigger retry, while permanent failures (invalid selectors, detached nodes) propagate immediately.

Response Handling and Promise Resolution

CDP responses return through the host bridge into browserRuntime(), which matches responses to pending promises by message ID. The runtime resolves helper promises with extracted values, completing the asynchronous flow back to the user script.

The event queue maintains history for observe introspection and debugging, with automatic eviction at the 10,000-entry cap to prevent unbounded growth.

Complete Data Flow Diagram


stdin
  ↓
src/index.ts (runMain/installEgoSdk) — wrap script, inject bindings
  ↓
src/run.ts (run) — create helperContext()
  ↓
src/helpers.ts — {nav, pointer, keyboard, js, $, observe}
  ↓
src/driver/*.ts — build CDP payloads, handle retries
  ↓
src/browser-runtime.ts — manage session, queue, ego.sendCDPMessage
  ↓
src/cdp-eval.ts (cdp/js) — final protocol serialization
  ↓
[Host bridge: ego.sendCDPMessage] → Embedded browser (CDP)
  ↑
Event queue ← CDP responses/events
  ↑
src/ref-map.ts + src/element-resolver.ts — ref resolution, re-snapshot
  ↑
Helper promises resolve → User script continues

Key Source Files and Responsibilities

File Path Primary Responsibility
package/ego-browser/src/index.ts CLI entry, stdio handling, script wrapping
package/ego-browser/src/run.ts Execution orchestration, context injection
package/ego-browser/src/helpers.ts Public API surface construction
package/ego-browser/src/browser-runtime.ts CDP session state, host bridge communication
package/ego-browser/src/cdp-eval.ts Low-level CDP and JS evaluation primitives
package/ego-browser/src/driver/nav.ts Navigation domain implementation
package/ego-browser/src/driver/pointer.ts Mouse input domain implementation
package/ego-browser/src/ref-map.ts Snapshot and ref-to-nodeID mapping
package/ego-browser/src/element-resolver.ts Locator/ref resolution, error classification
package/ego-browser/src/state.ts Global mutable runtime state

Summary

  • stdin JavaScript enters through src/index.ts, which wraps and prepares scripts for execution
  • Helper context injection in src/run.ts and src/helpers.ts exposes the automation API surface
  • Driver layer transformation in src/driver/*.ts converts intention to CDP semantics with retry logic
  • Browser runtime in src/browser-runtime.ts manages session state and host bridge communication
  • CDP primitives in src/cdp-eval.ts handle final protocol serialization for raw methods and JS evaluation
  • Ref resolution pipeline maintains DOM synchronization through snapshots in src/ref-map.ts and src/element-resolver.ts

Frequently Asked Questions

What makes ego-lite's stdin-to-browser pipeline different from Puppeteer or Playwright?

ego-lite eliminates the external browser process entirely. Rather than launching Chrome via Node's child_process, it relies on the host environment's ego.sendCDPMessage bridge—making the runtime a pure TypeScript translation layer without OS-level process management. This architecture removes networking overhead between Node and browser but requires the closed-source host to provide the actual CDP-capable instance.

How does ego-lite handle top-level await in scripts received through stdin?

The runMain() function in src/index.ts automatically wraps incoming scripts in an async IIFE before execution. This transformation occurs before the helper context injection, ensuring that top-level await expressions resolve correctly against the injected helper promises without requiring manual async function scoping from users.

What happens when a ref like @21 cannot be resolved in the current snapshot?

src/element-resolver.ts implements automatic recovery: when a ref lookup misses, it triggers observe.snapshot() to rebuild the ref-map from current DOM state, then retries resolution. If the ref remains unresolvable after snapshot refresh, the resolver throws ElementResolutionError with transient: false, indicating permanent failure rather than a retriable timing issue.

Where does the 10,000 event queue limit originate, and what happens when exceeded?

The cap is defined in src/browser-runtime.ts within the singleton state managed by browserRuntime(). When the event queue exceeds 10,000 entries, oldest events evict via standard array shift behavior. This prevents unbounded memory growth during long-running scripts while preserving recent history for observe debugging and event replay scenarios.

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 →