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 combinationsjs— JavaScript evaluation in page context$— element locator and ref resolutionobserve— 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 parametersjs(expression)— evaluates JavaScript in page context, automatically wrapping expressions lacking explicitreturnin 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.tsandsrc/helpers.tsexposes the automation API surface - Driver layer transformation in
src/driver/*.tsconverts intention to CDP semantics with retry logic - Browser runtime in
src/browser-runtime.tsmanages session state and host bridge communication - CDP primitives in
src/cdp-eval.tshandle final protocol serialization for raw methods and JS evaluation - Ref resolution pipeline maintains DOM synchronization through snapshots in
src/ref-map.tsandsrc/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →