# How Ego-Lite Handles JavaScript Execution from stdin for Agents: A Technical Deep Dive

> Learn how ego-lite handles JavaScript execution from stdin for agents. Discover its isolated execution, helper context, and output buffering for reliable agent runs.

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

---

**TLDR:** Ego-Lite's `ego-browser` CLI reads agent JavaScript from stdin (or a `stdinText` option), wraps it in an isolated async function with a prebuilt helper context, and executes it while buffering all output for reliable, deterministic agent runs.

Ego-lite is an open-source browser automation toolkit that lets AI agents drive real browsers through a simple JavaScript API. A central feature is the ability to run agent-supplied JavaScript from stdin, giving LLM-driven workflows a clean, scriptable entry point. According to the `citrolabs/ego-lite` repository, this mechanism is implemented in **[`package/ego-browser/src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/package/ego-browser/src/run.ts)** and involves a multi-step pipeline that ensures every script runs in a fresh, isolated context with access to the same helper API as the embedded SDK.

## The stdin Execution Pipeline in ego-lite

When you pipe JavaScript into `ego-browser`, the CLI doesn't just eval the code. It orchestrates a controlled execution environment through the `runMain` function. Here's how it works step by step.

### 1. CLI Argument Parsing

`runMain` first parses command-line flags like `--help`, `--doctor`, and `--reload`. If none are present, it proceeds to read the actual agent script from standard input. This happens early in the function (lines 61–71 in [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts)).

### 2. Reading the Script from stdin or a String

The script source is obtained either from an explicit `stdinText` option (for programmatic SDK use) or by consuming the entire stdin stream via a `readAll` utility. This dual-path approach means you can use ego-lite both as a shell tool and as a Node.js library.

### 3. Preparing the Execution Environment

Before running any agent code, ego-lite prepares a helper context. The `executionContext()` function loads the agent-facing helpers with `helpers.loadAgentHelpers` and assembles a public helper surface via `helpers.helperContext`. These helpers (like `page`, `browser`, `goto`, etc.) are then merged onto `globalThis`, so the agent script can call them directly without any imports.

### 4. Building an Isolated Async Function

The critical step is constructing a new function using the `AsyncFunction` constructor. Ego-lite builds a function whose parameters are the names of the helpers, and whose body is the supplied code prefixed with `"use strict";`. This technique:

- Prevents global pollution between runs
- Allows helper names to be scoped as function parameters
- Forces an async context so `await` works naturally

### 5. Executing and Cleaning Up

The constructed function is then invoked with the helper values. Any thrown errors are caught, and after execution ego-lite always stops any active screencast by calling `helpers.stopScreencast` before flushing buffered output (lines 17–31).

### 6. Output Handling with Buffering

`console.log` is overridden inside the script context to route messages through an internal output sink (`bufferOutput`). This buffered content is later flushed to the real stdout via `flushSink` once the script finishes or aborts. This guarantees that agent output appears in a correct order and is available even if the script ends abruptly.

## Code Examples: Running Ego-Lite Scripts from stdin

The simplest way to use ego-lite's stdin execution is through your shell:

```bash
ego-browser <<'JS'
await page.waitForLoadState()
console.log('Page title:', await page.title())
JS

```

You can use the exact same logic programmatically by passing a `stdinText` option:

```js
import { runMain } from "ego-browser";

const script = `
  await page.waitForLoadState()
  console.log('URL:', await page.url())
`;

await runMain({ stdinText: script });

```

For custom output handling, you can supply your own stdout/stderr streams:

```js
import { runMain } from "ego-browser";

await runMain({
  stdinText: `console.log('hello')`,
  stdout: { write: (msg) => process.stdout.write("[OUT] " + msg) },
  stderr: { write: (msg) => process.stderr.write("[ERR] " + msg) },
});

```

## Key Files Behind the stdin Execution Feature

The execution pipeline touches several source files. Here's a quick mapping:

| File | Role |
|------|------|
| [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) | Core CLI entry point; reads stdin, builds execution context, runs the agent script. |
| [`src/helpers.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/helpers.ts) | Loads and assembles the helper API exposed to agents (`helperContext`). |
| [`src/output-sink.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/output-sink.ts) | Buffers and flushes console output from the agent script. |
| [`src/index.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/index.ts) | Re-exports `runMain` for both CLI and SDK usage. |

## Summary

- Ego-lite reads agent scripts from stdin or a property, wraps them in a dedicated `AsyncFunction`, and injects a persisted helper context.
- The environment is isolated: scripts run with `"use strict"` and only see helper names as explicit parameters.
- console output is intercepted and buffered, so all logs are captured regardless of how the script exits.
- The same `runMain` works both from the `ego-browser` binary and as a Node.js import, making it easy to embed agent execution into your own tools.

## Frequently Asked Questions

### Can I run JavaScript code from a string instead of stdin?

Yes. The `runMain` function accepts a `stdinText` option that holds the exact JavaScript source, which is identical to using stdin. This is useful for programmatic SDK usage where you already have the script in memory (see [`src/run.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/run.ts) lines 94–99).

### How does ego-lite ensure the agent script has access to browser helpers?

Before execution, `executionContext` loads all agent helpers via `helpers.loadAgentHelpers` and expose them in a context object. That object is merged into `globalThis`, but the constructed async function also has each helper as a named parameter, giving scripts multiple ways to reference them.

### What happens if the agent script throws an error?

The `runMain` wrapper catches any errors thrown during execution, cleans up by calling `helpers.stopScreencast()`, and then flushes whatever buffered output was collected. Errors are logged to stderr, and the exit code is set accordingly, ensuring no silent failures.

### Is the output buffered or streamed in real time?

Ego-lite buffers all console output from an agent script with an internal sink (`bufferOutput`) and flushes it only after the execution finishes or aborts via `flushSink`. This ensures the agent sees a complete, consistent view of the output in one block.