Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8864c96e2 | ||
|
|
8432f6cd69 | ||
|
|
a8089310a6 | ||
|
|
ab92d2590b | ||
|
|
c1417c3c70 | ||
|
|
7cb69bd444 | ||
|
|
fb27835ebc | ||
|
|
2859da7b7c |
@@ -39,6 +39,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
fetch-depth: 0 # full history so the changelog step can diff tags
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -125,6 +126,29 @@ jobs:
|
||||
- name: List assets
|
||||
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
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
@@ -133,5 +157,8 @@ jobs:
|
||||
dist/*.tar.gz
|
||||
dist/*.tar.gz.sha256
|
||||
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
|
||||
|
||||
@@ -205,6 +205,47 @@ chrome-use --launch --profile auto open https://x.com/home
|
||||
|
||||
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
|
||||
|
||||
<img src="assets/shield.png" alt="stealth shield" width="320" align="right" />
|
||||
|
||||
Generated
+1
-1
@@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrome-use"
|
||||
version = "1.2.0"
|
||||
version = "1.2.2"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chrome-use"
|
||||
version = "1.2.0"
|
||||
version = "1.2.2"
|
||||
edition = "2021"
|
||||
description = "Fast browser automation CLI for AI agents"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -586,6 +586,17 @@ fn main() {
|
||||
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
|
||||
if env::var("AGENT_BROWSER_DAEMON").is_ok() {
|
||||
// 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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn to_ai_friendly_error(error: &str) -> String {
|
||||
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
|
||||
/// gone. This is what keeps commands on the tab the agent actually opened.
|
||||
fn resolved_active_index(&self) -> usize {
|
||||
if let Some(tid) = &self.active_target_id {
|
||||
if let Some(i) = self.pages.iter().position(|p| &p.target_id == tid) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
self.active_page_index
|
||||
resolve_active_index(
|
||||
&self.pages,
|
||||
self.active_target_id.as_deref(),
|
||||
self.active_page_index,
|
||||
)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
page.url = page_url.clone();
|
||||
page.title = title.clone();
|
||||
}
|
||||
self.pin_active_target();
|
||||
|
||||
let mut out = json!({ "url": page_url, "title": title });
|
||||
if let Some(w) = nav_warning {
|
||||
@@ -1124,6 +1151,9 @@ impl BrowserManager {
|
||||
target_type: "page".to_string(),
|
||||
});
|
||||
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?;
|
||||
|
||||
Ok(())
|
||||
@@ -2175,6 +2205,55 @@ mod tests {
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -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]
|
||||
fn browser_get_version_is_answered_locally() {
|
||||
// Liveness probe must NOT be forwarded (the extension can't do
|
||||
|
||||
+140
-1
@@ -1,5 +1,7 @@
|
||||
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");
|
||||
|
||||
@@ -7,6 +9,143 @@ const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
/// 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";
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
* Read by the popup status page. */
|
||||
let hostConnected = false
|
||||
let nextSession = 1
|
||||
/** tabId -> { sessionId, targetId } */
|
||||
const tabs = new Map()
|
||||
/** sessionId -> tabId (main session per tab) */
|
||||
@@ -261,7 +260,16 @@ async function attachTab(tabId) {
|
||||
const targetInfo = info?.targetInfo
|
||||
const targetId = String(targetInfo?.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 }
|
||||
tabs.set(tabId, entry)
|
||||
sessionToTab.set(sessionId, tabId)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"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.",
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB",
|
||||
"icons": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.1.3",
|
||||
|
||||
@@ -672,6 +672,8 @@ and [references/authentication.md](references/authentication.md).
|
||||
`chrome-use skills get electron`
|
||||
- **Slack workspace automation**: `chrome-use skills get slack`
|
||||
- **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`
|
||||
- **AWS Bedrock AgentCore cloud browser**: `chrome-use skills get agentcore`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user