Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8864c96e2 | ||
|
|
8432f6cd69 | ||
|
|
a8089310a6 | ||
|
|
ab92d2590b | ||
|
|
c1417c3c70 | ||
|
|
7cb69bd444 | ||
|
|
fb27835ebc | ||
|
|
2859da7b7c |
@@ -39,6 +39,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||||
|
fetch-depth: 0 # full history so the changelog step can diff tags
|
||||||
|
|
||||||
- name: Setup Rust toolchain
|
- name: Setup Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -125,6 +126,29 @@ jobs:
|
|||||||
- name: List assets
|
- name: List assets
|
||||||
run: ls -la dist
|
run: ls -la dist
|
||||||
|
|
||||||
|
# Build the changelog from conventional-commit subjects since the previous
|
||||||
|
# tag. GitHub's built-in generate_release_notes only lists merged PRs,
|
||||||
|
# which is near-empty for this commit-to-main workflow — so we render the
|
||||||
|
# commit log ourselves and every release shows what actually changed.
|
||||||
|
- name: Generate changelog
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
|
PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)"
|
||||||
|
{
|
||||||
|
echo "notes<<__NOTES_EOF__"
|
||||||
|
echo "## What changed"
|
||||||
|
echo ""
|
||||||
|
if [ -n "$PREV" ]; then
|
||||||
|
git log "${PREV}..${TAG}" --no-merges --pretty='- %s' | grep -v '^- chore(release)' || true
|
||||||
|
echo ""
|
||||||
|
echo "**Full changelog**: https://github.com/${{ github.repository }}/compare/${PREV}...${TAG}"
|
||||||
|
else
|
||||||
|
git log "${TAG}" --no-merges --pretty='- %s' | grep -v '^- chore(release)' || true
|
||||||
|
fi
|
||||||
|
echo "__NOTES_EOF__"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Attach to release
|
- name: Attach to release
|
||||||
uses: softprops/action-gh-release@v3
|
uses: softprops/action-gh-release@v3
|
||||||
with:
|
with:
|
||||||
@@ -133,5 +157,8 @@ jobs:
|
|||||||
dist/*.tar.gz
|
dist/*.tar.gz
|
||||||
dist/*.tar.gz.sha256
|
dist/*.tar.gz.sha256
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
# keep existing release notes if the release was created beforehand
|
# The commit-based changelog so every release shows what changed. The
|
||||||
|
# first matrix job to run creates the release with these notes;
|
||||||
|
# append_body:false keeps later platform jobs from duplicating them.
|
||||||
|
body: ${{ steps.changelog.outputs.notes }}
|
||||||
append_body: false
|
append_body: false
|
||||||
|
|||||||
@@ -205,6 +205,47 @@ chrome-use --launch --profile auto open https://x.com/home
|
|||||||
|
|
||||||
In CI environments, standalone mode is used automatically.
|
In CI environments, standalone mode is used automatically.
|
||||||
|
|
||||||
|
## Automated testing (`chrome-use test`)
|
||||||
|
|
||||||
|
Turn the repetitive "open it, click around, check it's right" work into a
|
||||||
|
**re-runnable suite** — unit tests for the frontend. Write cases in YAML; steps
|
||||||
|
reuse chrome-use's own commands and assertions compile to a single check:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# smoke.yaml
|
||||||
|
suite: chatgpt smoke
|
||||||
|
setup:
|
||||||
|
- account: chatgpt/huayue # inject a cookie-use login (optional)
|
||||||
|
cases:
|
||||||
|
- name: home loads logged in
|
||||||
|
steps:
|
||||||
|
- open: https://chatgpt.com/
|
||||||
|
- wait: { load: networkidle }
|
||||||
|
assert:
|
||||||
|
- url: { contains: chatgpt.com }
|
||||||
|
- visible: "#prompt-textarea"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chrome-use test smoke.yaml # launches an isolated browser, runs cases
|
||||||
|
chrome-use test smoke.yaml --session default # …or against your connected Chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
suite: chatgpt smoke (session cu-test)
|
||||||
|
✓ home loads logged in 1.2s
|
||||||
|
✗ composer takes text 0.8s
|
||||||
|
assert text "#prompt-textarea" contains "hi" → got ""
|
||||||
|
↳ cu-test-artifacts/composer-takes-text.png
|
||||||
|
2 cases · 1 passed · 1 failed
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code is non-zero if any case fails (drop it into CI), and failed cases save
|
||||||
|
a screenshot. Assertions: `url` · `visible` · `hidden` · `text` · `count` ·
|
||||||
|
`eval`. Steps: `open` · `click` · `fill` · `type` · `press` · `wait` · `scroll`
|
||||||
|
· `eval`. Full guide: `chrome-use skills get test`. Found a regression? Add a
|
||||||
|
case — the suite gets more valuable the more you use it.
|
||||||
|
|
||||||
## Anti-detection
|
## Anti-detection
|
||||||
|
|
||||||
<img src="assets/shield.png" alt="stealth shield" width="320" align="right" />
|
<img src="assets/shield.png" alt="stealth shield" width="320" align="right" />
|
||||||
|
|||||||
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.2.0"
|
version = "1.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "chrome-use"
|
name = "chrome-use"
|
||||||
version = "1.2.0"
|
version = "1.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast browser automation CLI for AI agents"
|
description = "Fast browser automation CLI for AI agents"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -586,6 +586,17 @@ fn main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hidden update-check worker, spawned detached by maybe_notify_update() to
|
||||||
|
// refresh the cached latest version without blocking a real command.
|
||||||
|
if env::args().nth(1).as_deref() == Some("__update-check") {
|
||||||
|
upgrade::run_update_check();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-blocking "update available" hint (stderr only; self-skips meta
|
||||||
|
// commands, daemon mode, CI, and the opt-out env vars).
|
||||||
|
upgrade::maybe_notify_update();
|
||||||
|
|
||||||
// Native daemon mode: when AGENT_BROWSER_DAEMON is set, run as the daemon process
|
// Native daemon mode: when AGENT_BROWSER_DAEMON is set, run as the daemon process
|
||||||
if env::var("AGENT_BROWSER_DAEMON").is_ok() {
|
if env::var("AGENT_BROWSER_DAEMON").is_ok() {
|
||||||
// Ignore SIGPIPE so the daemon isn't killed when the parent drops
|
// Ignore SIGPIPE so the daemon isn't killed when the parent drops
|
||||||
|
|||||||
@@ -136,6 +136,24 @@ fn active_page_index_after_removal(
|
|||||||
active_page_index
|
active_page_index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the session's active page index: prefer the pinned `active_target_id`
|
||||||
|
/// (stable across tab reorder / passive discovery / removal), falling back to the
|
||||||
|
/// raw `active_page_index` only when nothing is pinned or the pin is gone. Keeping
|
||||||
|
/// commands anchored to the pinned target is what stops `eval`/`get url`/`snapshot`
|
||||||
|
/// from drifting onto a foreign tab between commands (issue #14).
|
||||||
|
fn resolve_active_index(
|
||||||
|
pages: &[PageInfo],
|
||||||
|
active_target_id: Option<&str>,
|
||||||
|
active_page_index: usize,
|
||||||
|
) -> usize {
|
||||||
|
if let Some(tid) = active_target_id {
|
||||||
|
if let Some(i) = pages.iter().position(|p| p.target_id == tid) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
active_page_index
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts common error messages into AI-friendly, actionable descriptions.
|
/// Converts common error messages into AI-friendly, actionable descriptions.
|
||||||
pub fn to_ai_friendly_error(error: &str) -> String {
|
pub fn to_ai_friendly_error(error: &str) -> String {
|
||||||
let lower = error.to_lowercase();
|
let lower = error.to_lowercase();
|
||||||
@@ -762,12 +780,11 @@ impl BrowserManager {
|
|||||||
/// falling back to `active_page_index` when nothing is pinned or the pin is
|
/// falling back to `active_page_index` when nothing is pinned or the pin is
|
||||||
/// gone. This is what keeps commands on the tab the agent actually opened.
|
/// gone. This is what keeps commands on the tab the agent actually opened.
|
||||||
fn resolved_active_index(&self) -> usize {
|
fn resolved_active_index(&self) -> usize {
|
||||||
if let Some(tid) = &self.active_target_id {
|
resolve_active_index(
|
||||||
if let Some(i) = self.pages.iter().position(|p| &p.target_id == tid) {
|
&self.pages,
|
||||||
return i;
|
self.active_target_id.as_deref(),
|
||||||
}
|
self.active_page_index,
|
||||||
}
|
)
|
||||||
self.active_page_index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pin the current active page by target_id so later commands stick to it.
|
/// Pin the current active page by target_id so later commands stick to it.
|
||||||
@@ -854,10 +871,20 @@ impl BrowserManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An explicit `open`/navigate IS the "explicit open" the pin invariant is
|
||||||
|
// built around (see `active_target_id`). On the relay path `open` reuses an
|
||||||
|
// existing tab via this method rather than `add_page`, so without pinning
|
||||||
|
// here `active_target_id` stayed `None` and the session rode the fragile
|
||||||
|
// `active_page_index` — a later passive tab close/reorder then drifted
|
||||||
|
// `eval`/`get url`/`snapshot` onto a foreign tab between commands (issue
|
||||||
|
// #14). Sync the index to the resolved active page, then pin it by stable
|
||||||
|
// target_id so subsequent commands stick to the tab we just navigated.
|
||||||
|
self.active_page_index = self.resolved_active_index();
|
||||||
if let Some(page) = self.pages.get_mut(self.active_page_index) {
|
if let Some(page) = self.pages.get_mut(self.active_page_index) {
|
||||||
page.url = page_url.clone();
|
page.url = page_url.clone();
|
||||||
page.title = title.clone();
|
page.title = title.clone();
|
||||||
}
|
}
|
||||||
|
self.pin_active_target();
|
||||||
|
|
||||||
let mut out = json!({ "url": page_url, "title": title });
|
let mut out = json!({ "url": page_url, "title": title });
|
||||||
if let Some(w) = nav_warning {
|
if let Some(w) = nav_warning {
|
||||||
@@ -1124,6 +1151,9 @@ impl BrowserManager {
|
|||||||
target_type: "page".to_string(),
|
target_type: "page".to_string(),
|
||||||
});
|
});
|
||||||
self.active_page_index = 0;
|
self.active_page_index = 0;
|
||||||
|
// Pin this freshly-created tab (matches `add_page`) so it's a stable
|
||||||
|
// anchor from the first command, not a bare index (issue #14).
|
||||||
|
self.pin_active_target();
|
||||||
self.enable_domains(&attach_result.session_id).await?;
|
self.enable_domains(&attach_result.session_id).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -2175,6 +2205,55 @@ mod tests {
|
|||||||
assert_eq!(active_page_index_after_removal(0, 0, 0), 0);
|
assert_eq!(active_page_index_after_removal(0, 0, 0), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn page(target_id: &str) -> PageInfo {
|
||||||
|
PageInfo {
|
||||||
|
tab_id: 1,
|
||||||
|
label: None,
|
||||||
|
target_id: target_id.to_string(),
|
||||||
|
session_id: format!("session-{target_id}"),
|
||||||
|
url: String::new(),
|
||||||
|
title: String::new(),
|
||||||
|
target_type: "page".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- issue #14: a pinned target must keep commands on the right tab ---
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_active_index_prefers_pin_over_stale_index() {
|
||||||
|
// The tab we opened ("A") is at index 0, but `active_page_index` is stale
|
||||||
|
// and points at a foreign tab ("B"). With the pin set, resolution sticks
|
||||||
|
// to A — the drift that bit issue #14 (eval landing on /notifications).
|
||||||
|
let pages = vec![page("A"), page("B")];
|
||||||
|
assert_eq!(resolve_active_index(&pages, Some("A"), 1), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_active_index_unpinned_drifts_with_index() {
|
||||||
|
// Documents the pre-fix hazard: with no pin, resolution blindly trusts
|
||||||
|
// `active_page_index`, so a clamp/reorder from passive tab discovery lands
|
||||||
|
// commands on a foreign tab. This is exactly what pinning on `open` avoids.
|
||||||
|
let pages = vec![page("A"), page("B")];
|
||||||
|
assert_eq!(resolve_active_index(&pages, None, 1), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_active_index_falls_back_when_pin_is_gone() {
|
||||||
|
// If the pinned tab was closed (target_id no longer present), fall back to
|
||||||
|
// the index rather than panicking or returning a bogus slot.
|
||||||
|
let pages = vec![page("A"), page("B")];
|
||||||
|
assert_eq!(resolve_active_index(&pages, Some("CLOSED"), 1), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_active_index_pin_survives_passive_background_tab() {
|
||||||
|
// A foreign tab ("Z") gets appended by passive discovery after we pinned
|
||||||
|
// "A". The append doesn't shift A's position, and the pin keeps us on A
|
||||||
|
// regardless of what `active_page_index` happens to be.
|
||||||
|
let pages = vec![page("A"), page("B"), page("Z")];
|
||||||
|
assert_eq!(resolve_active_index(&pages, Some("A"), 2), 0);
|
||||||
|
}
|
||||||
|
|
||||||
// issue #7: removing the pinned active target must re-anchor the pin to a
|
// issue #7: removing the pinned active target must re-anchor the pin to a
|
||||||
// surviving page. Models `remove_page_by_target_id`'s index + re-pin steps
|
// surviving page. Models `remove_page_by_target_id`'s index + re-pin steps
|
||||||
// purely (BrowserManager needs a live CDP client, so the method itself can't
|
// purely (BrowserManager needs a live CDP client, so the method itself can't
|
||||||
|
|||||||
@@ -330,6 +330,45 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reattach_with_same_session_restores_target() {
|
||||||
|
// Issue #17 recovery contract. A tab's chrome.debugger session is torn
|
||||||
|
// down (cross-process nav, SW restart, …) then re-attached. The fix has
|
||||||
|
// the extension reuse the SAME `cb-tab-<tabId>` id across that churn, so
|
||||||
|
// after detach+reattach the relay must expose the NEW target under the
|
||||||
|
// SAME session — which is exactly the session the daemon is still bound
|
||||||
|
// to, so its eval/snapshot auto-follow the new page instead of going stale.
|
||||||
|
let mut s = RelayState::new();
|
||||||
|
s.handle_ext_message(&attached_event("T_old", "cb-tab-42"), "tok");
|
||||||
|
s.handle_ext_message(
|
||||||
|
&json!({
|
||||||
|
"method": "forwardCDPEvent",
|
||||||
|
"params": { "method": "Target.detachedFromTarget", "params": { "sessionId": "cb-tab-42" } }
|
||||||
|
}),
|
||||||
|
"tok",
|
||||||
|
);
|
||||||
|
s.handle_ext_message(&attached_event("T_new", "cb-tab-42"), "tok");
|
||||||
|
|
||||||
|
let route = s.route_client_command(1, &json!({ "id": 1, "method": "Target.getTargets" }));
|
||||||
|
match route {
|
||||||
|
ClientRoute::Local(v) => {
|
||||||
|
let infos = v["result"]["targetInfos"].as_array().unwrap();
|
||||||
|
assert_eq!(infos.len(), 1, "only the new target should remain");
|
||||||
|
assert_eq!(infos[0]["targetId"], "T_new");
|
||||||
|
}
|
||||||
|
_ => panic!("getTargets must be local"),
|
||||||
|
}
|
||||||
|
// The daemon's existing session id still resolves — to the new target.
|
||||||
|
let route = s.route_client_command(
|
||||||
|
1,
|
||||||
|
&json!({ "id": 2, "method": "Target.attachToTarget", "params": { "targetId": "T_new" } }),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
route,
|
||||||
|
ClientRoute::Local(json!({ "id": 2, "result": { "sessionId": "cb-tab-42" } }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn browser_get_version_is_answered_locally() {
|
fn browser_get_version_is_answered_locally() {
|
||||||
// Liveness probe must NOT be forwarded (the extension can't do
|
// Liveness probe must NOT be forwarded (the extension can't do
|
||||||
|
|||||||
+140
-1
@@ -1,5 +1,7 @@
|
|||||||
use crate::color;
|
use crate::color;
|
||||||
use std::process::{exit, Command};
|
use std::path::PathBuf;
|
||||||
|
use std::process::{exit, Command, Stdio};
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
@@ -7,6 +9,143 @@ const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|||||||
/// upgrade path and the install path are identical (GitHub Release, no npm).
|
/// upgrade path and the install path are identical (GitHub Release, no npm).
|
||||||
const INSTALL_URL: &str = "https://raw.githubusercontent.com/leeguooooo/chrome-use/main/install.sh";
|
const INSTALL_URL: &str = "https://raw.githubusercontent.com/leeguooooo/chrome-use/main/install.sh";
|
||||||
|
|
||||||
|
/// GitHub API for the latest published release (used by the update check).
|
||||||
|
const LATEST_RELEASE_API: &str =
|
||||||
|
"https://api.github.com/repos/leeguooooo/chrome-use/releases/latest";
|
||||||
|
|
||||||
|
/// Re-check the latest version at most this often (seconds).
|
||||||
|
const UPDATE_CHECK_INTERVAL_SECS: u64 = 86_400; // once a day
|
||||||
|
|
||||||
|
fn now_secs() -> u64 {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cache_path() -> PathBuf {
|
||||||
|
crate::connection::config_home().join("update-check.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_update_cache(checked_at: u64, latest: &str) {
|
||||||
|
let path = update_cache_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(parent);
|
||||||
|
}
|
||||||
|
let body = serde_json::json!({ "checked_at": checked_at, "latest": latest }).to_string();
|
||||||
|
let _ = std::fs::write(&path, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a dotted version (`1.2.1`, `v1.2.1`, `1.2.1-fork.3`) into a comparable
|
||||||
|
/// `(major, minor, patch)`, ignoring any pre-release/build suffix.
|
||||||
|
fn parse_version(v: &str) -> Option<(u64, u64, u64)> {
|
||||||
|
let core = v.trim().trim_start_matches('v');
|
||||||
|
let core = core.split(['-', '+']).next().unwrap_or(core);
|
||||||
|
let mut parts = core.split('.');
|
||||||
|
let major = parts.next()?.parse().ok()?;
|
||||||
|
let minor = parts.next().unwrap_or("0").parse().ok()?;
|
||||||
|
let patch = parts.next().unwrap_or("0").parse().ok()?;
|
||||||
|
Some((major, minor, patch))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_newer(latest: &str, current: &str) -> bool {
|
||||||
|
matches!((parse_version(latest), parse_version(current)), (Some(l), Some(c)) if l > c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hidden `__update-check` subcommand: fetch the latest release tag and cache it.
|
||||||
|
/// Spawned detached by [`maybe_notify_update`] so the network call never blocks a
|
||||||
|
/// real command. Uses `curl` (no extra deps, matches `upgrade`).
|
||||||
|
pub fn run_update_check() {
|
||||||
|
let latest = Command::new("curl")
|
||||||
|
.args([
|
||||||
|
"-fsSL",
|
||||||
|
"--max-time",
|
||||||
|
"8",
|
||||||
|
"-H",
|
||||||
|
"User-Agent: chrome-use-update-check",
|
||||||
|
LATEST_RELEASE_API,
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.ok()
|
||||||
|
.filter(|o| o.status.success())
|
||||||
|
.and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())
|
||||||
|
.and_then(|j| {
|
||||||
|
j.get("tag_name")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.trim_start_matches('v').to_string())
|
||||||
|
});
|
||||||
|
if let Some(latest) = latest {
|
||||||
|
write_update_cache(now_secs(), &latest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Non-blocking "update available" notice. Called once per command run:
|
||||||
|
/// - prints a one-line hint to **stderr** (never stdout, so `--json` is clean)
|
||||||
|
/// when a cached release is newer than the running binary;
|
||||||
|
/// - refreshes the cached latest version at most once a day via a **detached**
|
||||||
|
/// background process, so the current command never waits on the network.
|
||||||
|
///
|
||||||
|
/// Skipped for meta commands (upgrade/install/doctor/`__*`/--version/--help),
|
||||||
|
/// in CI, in daemon mode, and when CHROME_USE_NO_UPDATE_CHECK /
|
||||||
|
/// AGENT_BROWSER_NO_UPDATE_CHECK is set.
|
||||||
|
pub fn maybe_notify_update() {
|
||||||
|
if std::env::var_os("CHROME_USE_NO_UPDATE_CHECK").is_some()
|
||||||
|
|| std::env::var_os("AGENT_BROWSER_NO_UPDATE_CHECK").is_some()
|
||||||
|
|| std::env::var_os("CI").is_some()
|
||||||
|
|| std::env::var_os("AGENT_BROWSER_DAEMON").is_some()
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let first = std::env::args().nth(1).unwrap_or_default();
|
||||||
|
if first.starts_with("__")
|
||||||
|
|| matches!(
|
||||||
|
first.as_str(),
|
||||||
|
"upgrade" | "install" | "doctor" | "dashboard"
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if std::env::args().any(|a| matches!(a.as_str(), "--version" | "-V" | "--help" | "-h")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (checked_at, latest) = std::fs::read_to_string(update_cache_path())
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||||
|
.map(|j| {
|
||||||
|
(
|
||||||
|
j.get("checked_at").and_then(|v| v.as_u64()).unwrap_or(0),
|
||||||
|
j.get("latest")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or((0, String::new()));
|
||||||
|
|
||||||
|
if is_newer(&latest, CURRENT_VERSION) {
|
||||||
|
eprintln!(
|
||||||
|
"{} chrome-use {latest} is available (you have {CURRENT_VERSION}) — run `chrome-use upgrade`",
|
||||||
|
color::warning_indicator()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh in the background at most once a day. Bump the timestamp first
|
||||||
|
// (keeping the last-known latest) so concurrent runs don't all spawn a
|
||||||
|
// checker, then fire a detached child that does the network fetch.
|
||||||
|
if now_secs().saturating_sub(checked_at) >= UPDATE_CHECK_INTERVAL_SECS {
|
||||||
|
write_update_cache(now_secs(), &latest);
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
let _ = Command::new(exe)
|
||||||
|
.arg("__update-check")
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Upgrade to the latest GitHub Release.
|
/// Upgrade to the latest GitHub Release.
|
||||||
///
|
///
|
||||||
/// The stealth fork ships as a prebuilt binary attached to a GitHub Release —
|
/// The stealth fork ships as a prebuilt binary attached to a GitHub Release —
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ let port = null
|
|||||||
/** Whether the native-messaging host (the local chrome-use CLI) is linked.
|
/** Whether the native-messaging host (the local chrome-use CLI) is linked.
|
||||||
* Read by the popup status page. */
|
* Read by the popup status page. */
|
||||||
let hostConnected = false
|
let hostConnected = false
|
||||||
let nextSession = 1
|
|
||||||
/** tabId -> { sessionId, targetId } */
|
/** tabId -> { sessionId, targetId } */
|
||||||
const tabs = new Map()
|
const tabs = new Map()
|
||||||
/** sessionId -> tabId (main session per tab) */
|
/** sessionId -> tabId (main session per tab) */
|
||||||
@@ -261,7 +260,16 @@ async function attachTab(tabId) {
|
|||||||
const targetInfo = info?.targetInfo
|
const targetInfo = info?.targetInfo
|
||||||
const targetId = String(targetInfo?.targetId || '')
|
const targetId = String(targetInfo?.targetId || '')
|
||||||
if (!targetId) throw new Error('attachTab: no targetId')
|
if (!targetId) throw new Error('attachTab: no targetId')
|
||||||
const sessionId = `cb-tab-${nextSession++}`
|
// Derive the session id from the STABLE Chrome tabId, not a monotonic counter
|
||||||
|
// (issue #17). A tab's chrome.debugger session can be torn down and
|
||||||
|
// re-established — cross-process navigation, a service-worker restart wiping
|
||||||
|
// these in-memory maps, DevTools stealing the debugger — and each time the tab
|
||||||
|
// re-attaches. With a counter, re-attach minted a BRAND-NEW `cb-tab-N`, which
|
||||||
|
// orphaned the daemon's binding (it's still pinned to the old id and the relay
|
||||||
|
// never tells it to rebind) → permanent "stale sessionId / tab is gone". The
|
||||||
|
// tabId is stable across all of that, so `cb-tab-<tabId>` restores the SAME
|
||||||
|
// session the daemon already holds → eval/snapshot auto-follow the new page.
|
||||||
|
const sessionId = `cb-tab-${tabId}`
|
||||||
const entry = { sessionId, targetId }
|
const entry = { sessionId, targetId }
|
||||||
tabs.set(tabId, entry)
|
tabs.set(tabId, entry)
|
||||||
sessionToTab.set(sessionId, tabId)
|
sessionToTab.set(sessionId, tabId)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
"description": "Let chrome-use drive your logged-in Chrome \u2014 install once, no token, no per-use confirmation.",
|
||||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chrome-use",
|
"name": "chrome-use",
|
||||||
"version": "1.2.0",
|
"version": "1.2.2",
|
||||||
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
"description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@11.1.3",
|
"packageManager": "pnpm@11.1.3",
|
||||||
|
|||||||
@@ -672,6 +672,8 @@ and [references/authentication.md](references/authentication.md).
|
|||||||
`chrome-use skills get electron`
|
`chrome-use skills get electron`
|
||||||
- **Slack workspace automation**: `chrome-use skills get slack`
|
- **Slack workspace automation**: `chrome-use skills get slack`
|
||||||
- **Exploratory testing / QA / bug hunts**: `chrome-use skills get dogfood`
|
- **Exploratory testing / QA / bug hunts**: `chrome-use skills get dogfood`
|
||||||
|
- **Re-runnable test suites (frontend "unit tests")**: `chrome-use skills get test`
|
||||||
|
— turn repeated checks into a `chrome-use test <suite.yaml>` regression suite
|
||||||
- **Vercel Sandbox microVMs**: `chrome-use skills get vercel-sandbox`
|
- **Vercel Sandbox microVMs**: `chrome-use skills get vercel-sandbox`
|
||||||
- **AWS Bedrock AgentCore cloud browser**: `chrome-use skills get agentcore`
|
- **AWS Bedrock AgentCore cloud browser**: `chrome-use skills get agentcore`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user