| name | ever-browser |
| description | Use the Ever CLI to automate the active browser session, including ever exec page/browser globals, navigation, snapshots, clicks, input, screenshots, video recording, tabs, and raw browser actions. |
Ever Browser
Use the Ever CLI to automate the active browser session.
Prerequisites (check first)
Ever drives the user's real Chrome via the Ever extension. Before automating, the user must have:
- The Ever Chrome extension installed — https://chromewebstore.google.com/detail/ever/codfpjgkcdackkjhjhfefmfbckijjjnf
- Signed in (click the Ever toolbar icon → side panel → sign in with Google)
The CLI self-heals transient connection issues: a dead daemon is auto-restarted and the call retried, stale sessions retry transparently, and a stale pinned browser is switched off (you'll see a one-time notice on stderr). So if a command reports the browser is not connected / no browser available, retry once and/or run ever doctor first. Only if it persists does the user need to install the extension from the Chrome Web Store link above and sign in — don't tell them to reinstall on a single transient failure.
ever exec globals
ever exec injects two globals:
page: Playwright-style facade for common page actions. Prefer this for portable scripts.
browser: Ever-native lower-level API and escape hatch for tab/session helpers or unsupported actions.
Example:
await page.goto('https://example.com');
const snapshot = await browser.snapshot();
await page.click(12);
await page.fill(13, 'hello');
await page.keyboard.press('Enter');
page facade
Supported methods:
page.goto(url, opts?) routes to Ever navigation.
page.click(target) clicks a positive integer numeric snapshot id.
page.fill(target, text, opts?) replaces/clears an input value by default for a positive integer numeric snapshot id.
page.evaluate(fnOrString) evaluates JavaScript in the page context.
page.mouse.click(x, y, opts?) clicks viewport coordinates; supports { button, clickCount }.
page.keyboard.press(keys) sends keyboard keys.
page.waitForTimeout(ms) waits the requested milliseconds.
page.screenshot(options?) captures a viewport screenshot. The result is Ever's action result; screenshot bytes are in result.extracted_content.
Selector support is intentionally not implemented yet. For page.click and page.fill, pass a numeric snapshot id from browser.snapshot(). String selector targets throw an error like: selector targets are not supported yet; use a snapshot numeric ID.
page.evaluate(fnOrString) serialization semantics:
- Strings are passed as JavaScript expressions/statements to Ever page evaluation.
- Functions are stringified and invoked in the page context as zero-argument functions.
- Functions do not capture closures from the
ever exec script context.
- Additional arguments and functions with declared parameters are unsupported and throw a clear error.
browser Ever-native API
Use browser when you need Ever-specific behavior. All methods are async.
DOM & content:
browser.snapshot(opts?) — capture DOM; opts.mode is 'full' | 'incremental'
browser.pageEval(expression) — evaluate a JS string in the page context (the escape hatch behind page.evaluate; see Recipes for the single-line gotcha)
browser.extract(opts?) — page content as markdown; opts.format is 'text' | 'json' | 'both'
browser.screenshot(options?)
Interaction:
browser.click(id), browser.clickAt(x, y), browser.clickAtXY(x, y, opts?)
browser.doubleClick(idOrX, maybeY?), browser.rightClick(idOrX, maybeY?)
browser.input(id, text, opts?) — opts.clear, opts.delay
browser.scroll(direction, opts?) — direction is 'up' | 'down'; opts.amount, opts.id
browser.sendKeys(keys, opts?) — opts.charEvents for contenteditable / Sheets
browser.navigate(url, opts?) — opts.new_tab
browser.goBack() — navigate back in history
browser.wait(seconds)
Tabs & sessions:
browser.tabs(), browser.switchTab(tabId), browser.closeTab(tabId)
browser.start(opts?), browser.stop(opts?), browser.sessions(), browser.use(index), browser.status()
Domain helpers:
-
browser.sheets.selectCell(cell, opts?) — opts.noEdit
-
browser.fs.write(name, content, opts?) — opts.append; browser.fs.read(name, opts?) — opts.page; browser.fs.replace(name, oldStr, newStr)
-
browser.video.start(...), browser.video.stop(), browser.video.status() (see Video recording)
-
browser.action(name, params?) — raw action escape hatch for anything not wrapped above
Tab groups & session health
Ever organizes its tabs into browser tab groups. These helpers inspect group/tab
health and reclaim stale groups or excess tabs. Cleanup/prune default to a
dry run — pass an apply/dryRun: false flag to actually close anything.
ever exec API
await browser.groupHealth();
await browser.tabHealth();
await browser.cleanup({
staleOnly: true,
dryRun: true,
maxAgeMs,
includeActive: false,
includeAdoptedGroups: false,
});
await browser.prune({
session: true,
maxTabs,
dryRun: true,
includeActive: false,
includeAdoptedGroups: false,
});
await browser.findOrOpenTab({ url, newTab: true });
CLI commands
ever tab-health
ever cleanup --dry-run
ever cleanup --apply
ever prune --session --dry-run
ever prune --group <id> --apply
Session reuse & teardown
ever start --new forces a new session even if a matching one exists;
ever start --purpose <key> sets an explicit purpose key for reuse matching.
Via exec: browser.start({ new: true, purposeKey }).
ever stop --keep-tabs ends the session but preserves its visible tabs.
Via exec: browser.stop({ session: true, keepTabs: true }).
Video recording
Ever CLI can record the active CLI session to a local WebM file. Use this for
demo capture, bug reproduction evidence, or visual QA artifacts.
CLI commands
Typical live smoke:
ever start --url https://example.com
ever video-start demo.webm --size 800x600
ever exec "await page.goto('https://example.com'); await page.waitForTimeout(1000)"
ever video-stop --json
Useful commands:
ever video-start [output.webm] starts recording the active session.
ever video-start demo.webm --size 800x600 --quality 75 --fps 10
ever video-status reports active recording status, frame count, drops, and output path.
ever video-stop stops recording and finalizes the local WebM.
ever video-stop --json returns structured artifact metadata.
Recording writes the artifact on the local daemon machine, not the API server.
By default, output is saved under $EVER_HOME/recordings/ (e.g. ~/.ever/recordings/):
a bare filename like demo.webm lands there, while an absolute path is used as-is.
Local ffmpeg is required; missing or failing ffmpeg should be treated as a
real setup error, not silently ignored.
ever exec video API
ever exec exposes video controls on both facades:
await browser.video.start('demo.webm', {
size: { width: 800, height: 600 },
quality: 75,
fps: 10,
});
await page.goto('https://example.com');
await page.waitForTimeout(1000);
const result = await page.video.stop();
console.log(result.outputPath);
Supported helpers:
browser.video.start(outputPathOrOptions?, options?)
browser.video.stop()
browser.video.status()
page.video.start(outputPathOrOptions?, options?)
page.video.stop()
page.video.status()
Validation recipe
After recording, verify the local artifact instead of assuming success:
file demo.webm
ffprobe -hide_banner -loglevel error \
-select_streams v:0 \
-show_entries stream=codec_name,width,height,avg_frame_rate,pix_fmt \
-show_entries format=duration \
-of json demo.webm
Expected shape: file reports WebM; ffprobe reports a video stream such as
VP8/VP9 with non-zero dimensions. Recordings are variable frame rate and
preserve real timing, so format.duration should match the actual wall-clock
recording length (not frameCount / fps). Also check
video-status/video-stop --json for droppedFrames when diagnosing choppy
captures.
Timing model
- Recordings use true-to-life timing: each frame is stamped with its real
capture time (CDP
Page.screencastFrame metadata.timestamp) and encoded as
variable frame rate, so playback duration and pacing match reality.
- CDP emits frames on visual change, so idle periods stay compact and the file
is not padded to a fixed rate.
--fps is deprecated and no longer forces a constant rate; throttle capture
with --every-nth-frame instead.
Lifecycle notes
- Recording pins the tab active at
video-start; later tab switching does not
retarget the recording.
ever stop --session, daemon shutdown, tab removal, debugger detach, and frame
stream abort should all stop/cleanup recording.
- The API coordinates CDP
Page.startScreencast frames and acknowledges them;
the CLI daemon paces the frame stream to real capture timing and encodes
locally via ffmpeg.
Recipes
Reusable scraping scripts for specific sites live in recipes/. Before writing a
new scraper from scratch, check whether a recipe already exists.
| Recipe | Site | Task |
|---|
x-feed-scraper.js | x.com | Collect timeline posts (author, content, time, engagement) via the batch-scroll-collect pattern |
Each recipe documents its INIT / COLLECT / DUMP scripts, the CSS selectors it
relies on, and when they were last verified. To scrape a feed: run INIT to seed a
window._ accumulator, loop browser.pageEval('window.scrollBy(0, 1500)') +
COLLECT, then DUMP as JSON.
browser.pageEval gotcha: the evaluated code STRING must be a single line —
newlines in the string make pageEval return null silently (the code still
runs and side effects apply, but the return value is lost). Keep IIFEs on one
line, and avoid \d regex backslashes in the string (use .replace(/[^0-9]/g,'')).
After successfully scraping a new site, save the working script to
recipes/<site>-<task>.js in the same format.