Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c1bd718
feat(cli): add screencasting, screenshot downscaling, and heapsnapsho…
aeroxy Jun 25, 2026
7e8414d
fix(cli): resolve screenshot bounds validation, potential out-of-boun…
aeroxy Jun 25, 2026
71c08c7
fix(cli): refactor heap snapshot logic, progress report alignment, br…
aeroxy Jun 25, 2026
beae5a5
fix(cli): implement blocking task wrapping for snapshot parsing, push…
aeroxy Jun 25, 2026
138176f
fix(cli): optimize memory and parsing safety of heap snapshot lookups…
aeroxy Jun 25, 2026
50c5a58
fix(cli): implement safe node fields boundary validation, bypass repo…
aeroxy Jun 25, 2026
8d0278b
fix(cli): escape and sanitize special characters in node name text fo…
aeroxy Jun 25, 2026
8612ce6
fix(cli): integrate format structured outputs, scrolling coordinate o…
aeroxy Jun 25, 2026
adf6a78
fix: improve file I/O performance and format handling in memory and s…
aeroxy Jun 26, 2026
aa60228
refactor: encapsulate screenshot parameters into `ScreenshotOptions`
aeroxy Jun 26, 2026
7e98117
refactor: improve event handling and optimize screenshot scaling logic
aeroxy Jun 26, 2026
5575465
fix: reduce heap snapshot noise and improve screenshot metric fallback
aeroxy Jun 26, 2026
8c25aba
feat: improve robustness and reliability of memory snapshot and scree…
aeroxy Jun 26, 2026
9a897ab
feat: improve heap snapshot safety, offline inspection, and CLI path …
aeroxy Jun 26, 2026
50885a9
chore: update README
aeroxy Jun 26, 2026
c9e099d
refactor: remove unused inspect_heapsnapshot_node command and adjust …
aeroxy Jun 26, 2026
129fe29
fix: improve path resolution error handling and ensure valid dimensio…
aeroxy Jun 26, 2026
cfccabb
feat: enhance screenshot functionality with scroll offsets for non-fu…
aeroxy Jun 27, 2026
fb502b0
fix: ensure temporary heap snapshot files are removed on cancellation…
aeroxy Jun 27, 2026
3019bf1
fix: ensure proper file handle release before renaming heap snapshot …
aeroxy Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ src/
├── screenshot.rs
├── snapshot.rs
├── read_page.rs # read-page (Readability + HTML→Markdown)
├── memory.rs # take-heapsnapshot (CDP streaming) + inspect-heapsnapshot-node (offline)
├── evaluate.rs
├── input.rs # click/fill/type/press/hover
├── emulation.rs # emulate (viewport/geolocation/blocklist)
Expand Down Expand Up @@ -70,6 +71,18 @@ continuously collects `Network.*` and `Runtime.*` events. `console` and
All commands default to human-readable text. `--json` and `--toon` (compact,
LLM-friendly) produce structured output. Mutually exclusive.

### Offline Commands

`inspect-heapsnapshot-node` and `kill-daemon` are intercepted early in `run()`
before any Chrome connection or daemon spawn. `inspect-heapsnapshot-node` parses
a local `.heapsnapshot` file purely offline.

### Path Resolution

The daemon retains its startup CWD, so the CLI resolves all relative file-path
arguments (`--output`, `--file-path`) to absolute paths in `build_request`
before sending them to the daemon.

## Build & Test

```bash
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@ You can also use `--page <index>` for quick one-offs, or pass the raw hex target
|---------|-------------|
| `screenshot --output <path>` | Save screenshot to file |
| `screenshot --full-page` | Capture full scrollable page |
| `read-page` | Read page content as clean markdown (extracts main article) |
| `read-page --output <path>` | Save markdown to file |
| `screenshot --max-width <px> --max-height <px>` | Downscale screenshot to fit within dimensions |
| `read-page` | Read page content as clean Markdown (extracts main article) |
| `read-page --output <path>` | Save Markdown to file |
| `evaluate <expr> [--dialog-action <action>]` | Run JavaScript (optionally handle dialogs: accept, dismiss, or prompt text) |
| `snapshot` | Accessibility tree dump |
| `take-heapsnapshot --output <path>` | Capture V8 heap snapshot (streamed via CDP) |
| `inspect-heapsnapshot-node --file-path <path> --node-id <id>` | Inspect a node in a local `.heapsnapshot` file (offline, no Chrome needed) |

### Interaction

Expand Down Expand Up @@ -226,12 +229,10 @@ The daemon keeps a persistent CDP session on the current page to:
- Re-attach to a new target when `--target` changes (the previous target's event buffers are discarded on the switch).

## Source layout

-```
+```text
src/
├── main.rs # Entry point + daemon dispatch

```text
src/
├── main.rs # Entry point + daemon dispatch
├── lib.rs # CLI (clap) + command routing
├── cdp.rs # Raw CDP over WebSocket (JSON-RPC) + persistent session
├── browser.rs # Auto-connect (DevToolsActivePort)
Expand All @@ -251,6 +252,7 @@ The daemon keeps a persistent CDP session on the current page to:
├── screenshot.rs
├── snapshot.rs
├── read_page.rs # read-page (Readability extraction + HTML→Markdown)
├── memory.rs # take-heapsnapshot (CDP streaming) + inspect-heapsnapshot-node (offline)
├── evaluate.rs
├── executor.rs # Command dispatch + persistent-session reuse
├── input.rs # click/fill/type/press/hover
Expand Down
2 changes: 1 addition & 1 deletion src/cdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ impl CdpClient {
std::mem::take(&mut self.console_events).into()
}

fn push_event(&mut self, event: Value) {
pub(crate) fn push_event(&mut self, event: Value) {
// Only route events into the persistent buffers when the event's
// sessionId matches the persistent page session (flatten-mode events
// are tagged with sessionId). Events from ad-hoc sessions (sw-logs
Expand Down
44 changes: 38 additions & 6 deletions src/commands/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ pub fn known_args(cmd: &str) -> &'static [&'static str] {
"clear_all",
"output",
],
"screenshot" => &["output", "format", "full_page"],
"screenshot" => &[
"output",
"format",
"full_page",
"quality",
"max_width",
"max_height",
],
"evaluate" => &["expression", "dialog_action", "output", "track_navigation"],
"click" => &["selector"],
"click-at" => &["x", "y"],
Expand All @@ -46,6 +53,8 @@ pub fn known_args(cmd: &str) -> &'static [&'static str] {
"hover" => &["selector"],
"snapshot" => &["output"],
"read-page" => &["output"],
"take-heapsnapshot" => &["output"],
"inspect-heapsnapshot-node" => &["file_path", "node_id"],
"emulate" => &[
"viewport",
"device_scale_factor",
Expand Down Expand Up @@ -97,6 +106,13 @@ fn validate_args(cmd: &str, args: &serde_json::Value) -> Result<()> {
}

/// Whether a command operates at the browser level (no page session needed).
///
/// `inspect-heapsnapshot-node` is intentionally excluded: it is intercepted
/// offline in the CLI before any daemon connection is established, so the
/// daemon should never receive it. If it ever does, omitting it here lets it
/// fall through to `inner_execute`'s catch-all `bail!("Unknown command")`
/// rather than hitting the `_ => unreachable!()` arm in the browser-level
/// dispatch and panicking.
fn is_browser_level(cmd: &str) -> bool {
matches!(cmd, "list-pages" | "new-page" | "sw-logs" | "kill-daemon")
}
Comment thread
aeroxy marked this conversation as resolved.
Expand Down Expand Up @@ -361,11 +377,21 @@ async fn inner_execute(
commands::screenshot::take_screenshot(
client,
session_id,
args.get("output").and_then(|v| v.as_str()),
args.get("format").and_then(|v| v.as_str()).unwrap_or("png"),
args.get("full_page")
.and_then(|v| v.as_bool())
.unwrap_or(false),
commands::screenshot::ScreenshotOptions {
output: args.get("output").and_then(|v| v.as_str()).map(String::from),
format: args
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("png")
.to_string(),
full_page: args
.get("full_page")
.and_then(|v| v.as_bool())
.unwrap_or(false),
quality: args.get("quality").and_then(|v| v.as_u64()),
max_width: args.get("max_width").and_then(|v| v.as_f64()),
max_height: args.get("max_height").and_then(|v| v.as_f64()),
},
)
.await
}
Expand Down Expand Up @@ -441,6 +467,12 @@ async fn inner_execute(
)
.await
}
"take-heapsnapshot" => match args.get("output").and_then(|v| v.as_str()) {
Some(output) => {
commands::memory::take_heapsnapshot(client, session_id, output, req.format()).await
}
None => bail!("output required"),
},
"emulate" => {
// block/unblock come from the global request fields (the single flag
// definition); the emulate handler applies them itself — in the right
Expand Down
Loading