Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c85e3faa82 | ||
|
|
b25958946c | ||
|
|
d4ff49caa8 | ||
|
|
4e949fffbf | ||
|
|
af46490812 |
@@ -152,9 +152,16 @@ jobs:
|
||||
# Group commit subjects by conventional-commit type so the notes are
|
||||
# scannable ("what's new / what's fixed") instead of a flat dev log.
|
||||
LOG="$(git log "$RANGE" --no-merges --pretty='%s' | grep -v '^chore(release)' || true)"
|
||||
# NOTE: the job runs under `bash -e`. grep returning 1 (no match) and
|
||||
# the `[ -n "$body" ]` test returning 1 (empty section) must NOT abort
|
||||
# the script — otherwise a release whose commit range lacks a whole
|
||||
# category (e.g. only `feat`, no `fix`) dies before writing the closing
|
||||
# heredoc delimiter and the whole release step fails. `|| true` +
|
||||
# `return 0` keep section() always-succeeding.
|
||||
section() { # $1=header $2=grep-pattern
|
||||
local body; body="$(printf '%s\n' "$LOG" | grep -E "$2" | sed 's/^/- /')"
|
||||
[ -n "$body" ] && { printf '\n### %s\n%s\n' "$1" "$body"; }
|
||||
local body; body="$(printf '%s\n' "$LOG" | grep -E "$2" | sed 's/^/- /' || true)"
|
||||
[ -n "$body" ] && printf '\n### %s\n%s\n' "$1" "$body"
|
||||
return 0
|
||||
}
|
||||
{
|
||||
echo "notes<<__NOTES_EOF__"
|
||||
|
||||
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrome-use"
|
||||
version = "1.5.1"
|
||||
version = "1.5.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chrome-use"
|
||||
version = "1.5.1"
|
||||
version = "1.5.3"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
license = "Apache-2.0"
|
||||
|
||||
+92
-18
@@ -30,12 +30,65 @@ pub enum ParseError {
|
||||
InvalidSessionName { name: String },
|
||||
}
|
||||
|
||||
/// Top-level commands an agent is likely to mistype, used for "did you mean"
|
||||
/// suggestions on an unknown command (issue #29). Not exhaustive — just the
|
||||
/// common verbs plus a few known wrong-guesses mapped to the real command.
|
||||
const KNOWN_COMMANDS: &[&str] = &[
|
||||
"open", "navigate", "click", "fill", "type", "press", "snapshot", "screenshot", "eval", "get",
|
||||
"text", "html", "frames", "find", "wait", "scroll", "hover", "select", "check", "uncheck",
|
||||
"tab", "tabs", "close", "back", "forward", "reload", "sessions", "status", "daemon", "doctor",
|
||||
"upgrade", "connect", "cookies", "mouse", "keyboard", "stream", "frame", "profiles", "title",
|
||||
"url", "is", "drag", "dialog", "upload",
|
||||
];
|
||||
|
||||
/// Levenshtein distance, capped — small inputs only (command names).
|
||||
fn edit_distance(a: &str, b: &str) -> usize {
|
||||
let a: Vec<char> = a.chars().collect();
|
||||
let b: Vec<char> = b.chars().collect();
|
||||
let mut prev: Vec<usize> = (0..=b.len()).collect();
|
||||
let mut curr = vec![0usize; b.len() + 1];
|
||||
for (i, &ca) in a.iter().enumerate() {
|
||||
curr[0] = i + 1;
|
||||
for (j, &cb) in b.iter().enumerate() {
|
||||
let cost = if ca == cb { 0 } else { 1 };
|
||||
curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
|
||||
}
|
||||
std::mem::swap(&mut prev, &mut curr);
|
||||
}
|
||||
prev[b.len()]
|
||||
}
|
||||
|
||||
/// Closest known command within a small edit distance, or a prefix/substring
|
||||
/// match — `None` if nothing is close enough to suggest confidently.
|
||||
fn nearest_command(input: &str) -> Option<String> {
|
||||
let lower = input.to_lowercase();
|
||||
// Exact prefix/substring hits first (e.g. "session" -> "sessions").
|
||||
if let Some(c) = KNOWN_COMMANDS
|
||||
.iter()
|
||||
.find(|c| c.starts_with(&lower) || lower.starts_with(**c))
|
||||
{
|
||||
return Some(c.to_string());
|
||||
}
|
||||
// Tolerance scales with length: short words get distance 1, longer get 2.
|
||||
let max_dist = if lower.len() <= 4 { 1 } else { 2 };
|
||||
KNOWN_COMMANDS
|
||||
.iter()
|
||||
.map(|c| (*c, edit_distance(&lower, c)))
|
||||
.filter(|(_, d)| *d <= max_dist)
|
||||
.min_by_key(|(_, d)| *d)
|
||||
.map(|(c, _)| c.to_string())
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
pub fn format(&self) -> String {
|
||||
match self {
|
||||
ParseError::UnknownCommand { command } => {
|
||||
format!("Unknown command: {}", command)
|
||||
}
|
||||
ParseError::UnknownCommand { command } => match nearest_command(command) {
|
||||
Some(suggestion) => format!(
|
||||
"Unknown command: {}\nDid you mean: chrome-use {}?",
|
||||
command, suggestion
|
||||
),
|
||||
None => format!("Unknown command: {}", command),
|
||||
},
|
||||
ParseError::UnknownSubcommand {
|
||||
subcommand,
|
||||
valid_options,
|
||||
@@ -2410,16 +2463,16 @@ fn parse_get(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
if main {
|
||||
return Ok(json!({ "id": id, "action": "gettext", "main": true }));
|
||||
}
|
||||
// `get text` with no selector returns the whole page's text (body) —
|
||||
// a common convenience; previously it errored without a selector
|
||||
// (issue #24-D).
|
||||
let sel = rest
|
||||
.iter()
|
||||
.skip(1)
|
||||
.find(|a| !a.starts_with("--"))
|
||||
.copied()
|
||||
.unwrap_or("body");
|
||||
Ok(json!({ "id": id, "action": "gettext", "selector": sel }))
|
||||
// `get text` with no selector reads the WHOLE PAGE and now defaults
|
||||
// to cross-frame aggregation, so an agent gets a page's iframed
|
||||
// content (listing descriptions etc.) without having to know about
|
||||
// `--all-frames` (#27). On a single-frame page this is identical to
|
||||
// the old body read; multi-frame pages get the child frames too —
|
||||
// a strict superset. An explicit selector stays element-scoped.
|
||||
match rest.iter().skip(1).find(|a| !a.starts_with("--")).copied() {
|
||||
Some(sel) => Ok(json!({ "id": id, "action": "gettext", "selector": sel })),
|
||||
None => Ok(json!({ "id": id, "action": "gettext", "allFrames": true })),
|
||||
}
|
||||
}
|
||||
Some("html") => {
|
||||
let sel = rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -4777,15 +4830,21 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_text_defaults_to_body() {
|
||||
// `get text` with no selector now returns the whole page (body) instead
|
||||
// of erroring (issue #24-D).
|
||||
fn test_get_text_defaults_to_all_frames() {
|
||||
// `get text` with no selector now reads the whole page across ALL frames
|
||||
// by default (#27), so iframed content isn't silently missed. (Was: a
|
||||
// top-frame `body` read, #24-D.)
|
||||
let cmd = parse_command(&args("get text"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "gettext");
|
||||
assert_eq!(cmd["selector"], "body");
|
||||
// An explicit selector still wins.
|
||||
assert_eq!(cmd["allFrames"], true);
|
||||
assert!(cmd.get("selector").is_none());
|
||||
// An explicit selector still wins and stays element-scoped.
|
||||
let cmd2 = parse_command(&args("get text h1"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd2["selector"], "h1");
|
||||
assert!(cmd2.get("allFrames").is_none());
|
||||
// `text` top-level shortcut behaves the same.
|
||||
let cmd3 = parse_command(&args("text"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd3["allFrames"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4813,6 +4872,21 @@ mod tests {
|
||||
assert_eq!(cmd["action"], "frames");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nearest_command_suggestions() {
|
||||
assert_eq!(nearest_command("sesions").as_deref(), Some("sessions"));
|
||||
assert_eq!(nearest_command("session").as_deref(), Some("sessions"));
|
||||
assert_eq!(nearest_command("clik").as_deref(), Some("click"));
|
||||
assert_eq!(nearest_command("screenshits").as_deref(), Some("screenshot"));
|
||||
// Nonsense with no close match stays silent.
|
||||
assert_eq!(nearest_command("xyzzy"), None);
|
||||
// The unknown-command error embeds the suggestion.
|
||||
let err = ParseError::UnknownCommand {
|
||||
command: "sesions".to_string(),
|
||||
};
|
||||
assert!(err.format().contains("Did you mean: chrome-use sessions?"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_text_main() {
|
||||
for variant in ["get text --main", "get text --readable", "text -m"] {
|
||||
|
||||
@@ -893,6 +893,14 @@ fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// `sessions` is a natural top-level guess for "list my sessions" (the skill
|
||||
// advertises sessions as a feature) — route it to the daemon inventory the
|
||||
// same way `daemon status` does (issue #29).
|
||||
if clean.first().map(|s| s.as_str()) == Some("sessions") {
|
||||
run_daemon(&["sessions".to_string(), "status".to_string()], flags.json);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle close --all: close all active sessions
|
||||
if matches!(
|
||||
clean.first().map(|s| s.as_str()),
|
||||
|
||||
@@ -4561,7 +4561,22 @@ async fn handle_tab_switch(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
||||
state.ref_map.clear();
|
||||
state.iframe_sessions.clear();
|
||||
state.active_frame_id = None;
|
||||
let result = mgr.tab_switch_by_id(tab_id).await?;
|
||||
let mut result = mgr.tab_switch_by_id(tab_id).await?;
|
||||
|
||||
// Liveness probe: confirm the new session actually answers before we report
|
||||
// success, so `tab <id>` doesn't print a misleading ✓ for a session that's
|
||||
// stale and will fail on the very next command (issue #29.3). On the churned
|
||||
// -tabId case the ext-0.4.9 targetId recovery (#24) self-heals within ~6s, so
|
||||
// we surface a warning rather than a hard error to avoid a false failure
|
||||
// during that window.
|
||||
if mgr.evaluate("1", None).await.is_err() {
|
||||
if let Some(obj) = result.as_object_mut() {
|
||||
obj.insert(
|
||||
"warning".to_string(),
|
||||
json!("switched tab is not responding yet (session re-attaching); retry the next command"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// `--activate`: raise this tab to the foreground (the switch made it active;
|
||||
// bring_to_front acts on the active tab) — for handing a specific tab to the
|
||||
|
||||
+19
-17
@@ -597,19 +597,21 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou
|
||||
// Tab switch
|
||||
if action == Some("tab_switch") {
|
||||
if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_str()) {
|
||||
if let Some(url) = data.get("url").and_then(|v| v.as_str()) {
|
||||
println!(
|
||||
"{} Switched to tab [{}] ({})",
|
||||
color::success_indicator(),
|
||||
tab_id,
|
||||
url
|
||||
);
|
||||
let warning = data.get("warning").and_then(|v| v.as_str());
|
||||
// A non-responding session isn't a real success — show a warning
|
||||
// indicator instead of the green ✓ (issue #29.3).
|
||||
let indicator = if warning.is_some() {
|
||||
color::warning_indicator()
|
||||
} else {
|
||||
println!(
|
||||
"{} Switched to tab [{}]",
|
||||
color::success_indicator(),
|
||||
tab_id
|
||||
);
|
||||
color::success_indicator()
|
||||
};
|
||||
if let Some(url) = data.get("url").and_then(|v| v.as_str()) {
|
||||
println!("{} Switched to tab [{}] ({})", indicator, tab_id, url);
|
||||
} else {
|
||||
println!("{} Switched to tab [{}]", indicator, tab_id);
|
||||
}
|
||||
if let Some(w) = warning {
|
||||
eprintln!("{}", color::dim(w));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1957,8 +1959,7 @@ Usage: chrome-use get <subcommand> [args]
|
||||
Retrieves various types of information from elements or the page.
|
||||
|
||||
Subcommands:
|
||||
text <selector> Get text content of element
|
||||
text --all-frames Aggregate text across ALL frames (incl. iframes)
|
||||
text [selector] Element text; no selector = WHOLE PAGE, all frames
|
||||
text --main Main-content text only (skip nav/header/sidebar)
|
||||
html <selector> Get inner HTML of element
|
||||
value <selector> Get value of input element
|
||||
@@ -1975,8 +1976,8 @@ Global Options:
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
chrome-use get text @e1
|
||||
chrome-use get text --all-frames # read iframed content (listing pages)
|
||||
chrome-use get text # whole page across ALL frames (default)
|
||||
chrome-use get text @e1 # one element
|
||||
chrome-use get text --main # main content, no nav/sidebar boilerplate
|
||||
chrome-use frames # list frames + where the text lives
|
||||
chrome-use get html "#content"
|
||||
@@ -3188,7 +3189,7 @@ Navigation:
|
||||
|
||||
Get Info: chrome-use get <what> [selector]
|
||||
text, html, value, attr <name>, title, url, count, box, styles, cdp-url
|
||||
text --all-frames (cross-frame), text --main (no boilerplate), frames (list)
|
||||
text (no selector = whole page, all frames), text --main, frames (list)
|
||||
|
||||
Check State: chrome-use is <what> <selector>
|
||||
visible, enabled, checked
|
||||
@@ -3287,6 +3288,7 @@ Confirmation:
|
||||
Sessions:
|
||||
session Show current session name
|
||||
session list List active sessions
|
||||
sessions List running session daemons (alias of daemon status)
|
||||
daemon status List running session daemons (+ relay state)
|
||||
daemon restart Kill all session daemons; keeps the extension relay
|
||||
up. Clears stale/cross-leaked state after an upgrade.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "chrome-use",
|
||||
"version": "1.5.1",
|
||||
"version": "1.5.3",
|
||||
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.1.3",
|
||||
|
||||
@@ -207,8 +207,8 @@ assigned fresh on every snapshot.
|
||||
For unstructured reading (no refs needed):
|
||||
|
||||
```bash
|
||||
chrome-use get text @e1 # visible text of an element
|
||||
chrome-use get text --all-frames # whole page, aggregated across ALL frames
|
||||
chrome-use get text # WHOLE PAGE — all frames by default (see below)
|
||||
chrome-use get text @e1 # visible text of one element (or a CSS selector)
|
||||
chrome-use get text --main # main content only — skip nav/header/sidebar
|
||||
chrome-use frames # list every frame + where the text lives
|
||||
chrome-use get html @e1 # innerHTML
|
||||
@@ -219,13 +219,20 @@ chrome-use get url # current URL
|
||||
chrome-use get count ".item" # count matching elements
|
||||
```
|
||||
|
||||
On listing/marketplace pages (Yahoo Auctions, Rakuten, Mercari shops) the seller's
|
||||
description often lives in a **child frame** or is buried under a "related items"
|
||||
sidebar, so a plain `get text body` returns only header/nav boilerplate. When the
|
||||
text you expect is missing: run `chrome-use frames` to see where it is, then
|
||||
`get text --all-frames` (reads every reachable frame incl. cross-origin iframes)
|
||||
or `get text --main` (drops the global chrome). If the content is lazy-loaded,
|
||||
`scroll` it into view first.
|
||||
**Whole-page text is cross-frame by default.** `chrome-use get text` with no
|
||||
selector aggregates visible text across **every** frame — top document plus
|
||||
same-process child frames plus cross-origin iframes — so you never silently miss
|
||||
content that lives in an iframe (Yahoo Auctions / Rakuten / Mercari shop
|
||||
descriptions, embedded checkout/spec frames). Each child frame is delimited with
|
||||
a `----- frame [kind] url -----` marker. You do **not** need to remember a flag —
|
||||
the default already reads all frames. (`--all-frames` is still accepted as an
|
||||
explicit alias.)
|
||||
|
||||
So: when text looks missing or wrong, you don't have to guess — just
|
||||
`chrome-use get text` reads everything. To **see** the structure (which frame
|
||||
holds what), run `chrome-use frames`. To **cut boilerplate** (global nav/header/
|
||||
footer, "related items" sidebars), use `chrome-use get text --main`. If content
|
||||
is lazy-loaded, `scroll` it into view first, then read.
|
||||
|
||||
## Interacting
|
||||
|
||||
|
||||
Reference in New Issue
Block a user