From d81bc01645500fe9ea8ed026565b12c2e4139feb Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Wed, 17 Jun 2026 16:02:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(site):=20bb-sites=20adapters=20=E2=80=94?= =?UTF-8?q?=20turn=20any=20site=20into=20a=20structured-data=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `chrome-use site` — run community bb-sites adapters over chrome-use's stealth transport. An adapter is a per-command JS function that calls a site's own JSON API from inside your logged-in tab (your cookies, same-origin fetch, the site's modules) and returns clean JSON — no clicking/scraping. - site update fetch the upstream bb-sites pack into ~/.chrome-use/sites - site list list installed adapters (name/cmd) - site info show an adapter's @meta (args, domain, capabilities) - site / [args] navigate to its domain (reuse tab if already there) + eval, return JSON chrome-use ships zero adapter code; `site update` fetches epiral/bb-sites at runtime (like a package manager). Adapters remain their authors' property. cli/src/site.rs (load/parse/build_eval/list/update/map_args + tests), wired via commands.rs (parse), actions.rs (handle_site), main.rs (CLI dispatch). Docs in README, README.zh, skill-data/core. Verified live: github/issues returned 30 real issues as JSON over the relay. --- README.md | 28 ++++ README.zh.md | 25 ++++ cli/Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/commands.rs | 42 ++++++ cli/src/main.rs | 79 +++++++++++ cli/src/native/actions.rs | 42 ++++++ cli/src/output.rs | 8 ++ cli/src/site.rs | 267 ++++++++++++++++++++++++++++++++++++++ package.json | 2 +- skill-data/core/SKILL.md | 22 ++++ 11 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 cli/src/site.rs diff --git a/README.md b/README.md index 73fc7d9..5f3e4c5 100644 --- a/README.md +++ b/README.md @@ -205,6 +205,34 @@ chrome-use --launch --profile auto open https://x.com/home In CI environments, standalone mode is used automatically. +## Site adapters — turn a website into a structured-data CLI + +Most "read GitHub issues" / "search Reddit" / "get my Bilibili feed" tasks don't +need clicking and screenshotting at all — the site already has a JSON API behind +its own login. A **site adapter** is a tiny JS function that calls that API *from +inside your logged-in tab* (your cookies, same-origin `fetch`, the site's own +modules) and returns clean JSON. The site can't tell it apart from you, because it +*is* you. + +chrome-use ships none of these adapters — `site update` fetches the community +[**bb-sites**](https://github.com/epiral/bb-sites) pack at runtime (like a package +manager pulling a dependency), then runs them over chrome-use's stealth transport: + +```bash +chrome-use site update # fetch the adapter pack (~145 commands) +chrome-use site list # github/issues, reddit/search, bilibili/feed, … +chrome-use site info github/issues # see an adapter's args + domain + +# Run one — navigates to the site (reusing the tab if you're already there) and returns JSON +chrome-use site github/issues epiral/bb-browser --json +chrome-use site reddit/search "rust async" --json +chrome-use site bilibili/feed --json # works because it's your logged-in session +``` + +Positional args fill the adapter's declared args in order; `--key value` overrides +by name. Adapters are authored by the bb-sites community and remain their authors' +property — chrome-use just runs them. + ## Automated testing (`chrome-use test`) Turn the repetitive "open it, click around, check it's right" work into a diff --git a/README.zh.md b/README.zh.md index 5187f41..e13db94 100644 --- a/README.zh.md +++ b/README.zh.md @@ -163,6 +163,31 @@ chrome-use --launch --profile auto open https://x.com/home # 或显式指定:--profile Default / --profile "Profile 1" ``` +## 站点适配器 —— 把一个网站变成「结构化数据 CLI」 + +大多数「读 GitHub issue」「搜 Reddit」「拉我的 B 站动态」这类任务,根本不需要点击 + +截图 —— 网站登录态背后本来就有 JSON 接口。**站点适配器**就是一小段 JS 函数,它在你 +**已登录的标签页内**调用那个接口(用你的 cookie、同源 `fetch`、网站自己的模块),返回 +干净的 JSON。网站分辨不出它和你的区别,因为它**就是你**。 + +chrome-use 本身不附带任何适配器 —— `site update` 会在运行时拉取社区的 +[**bb-sites**](https://github.com/epiral/bb-sites) 适配器包(就像包管理器拉依赖), +然后在 chrome-use 的隐身通道上运行它们: + +```bash +chrome-use site update # 拉取适配器包(约 145 条命令) +chrome-use site list # github/issues、reddit/search、bilibili/feed… +chrome-use site info github/issues # 查看某个适配器的参数 + 域名 + +# 运行一个 —— 会导航到对应站点(已在该站点则复用当前标签页)并返回 JSON +chrome-use site github/issues epiral/bb-browser --json +chrome-use site reddit/search "rust async" --json +chrome-use site bilibili/feed --json # 能用,因为走的是你的登录态 +``` + +位置参数按适配器声明的参数顺序填入;`--key value` 按名覆盖。适配器由 bb-sites 社区编写、 +版权归各自作者所有 —— chrome-use 只负责运行它们。 + ## 自动化测试(`chrome-use test`) 把反复的「打开它、点一圈、看对不对」变成**可重跑的测试套件** —— 前端的单元测试。用 YAML 写用例;步骤复用 chrome-use 自己的命令,断言编译成一次检查: diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 3eff4b5..1c54e5d 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -290,7 +290,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrome-use" -version = "1.5.18" +version = "1.5.19" dependencies = [ "aes", "aes-gcm", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 109d3c0..b725a0f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chrome-use" -version = "1.5.18" +version = "1.5.19" edition = "2021" description = "Fast browser automation CLI for AI agents" license = "Apache-2.0" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 9d49eff..1515fa2 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -78,6 +78,7 @@ const KNOWN_COMMANDS: &[&str] = &[ "drag", "dialog", "upload", + "site", ]; /// Levenshtein distance, capped — small inputs only (command names). @@ -1193,6 +1194,47 @@ fn parse_command_inner(args: &[String], flags: &Flags) -> Result { + // `site / [positional...] [--key value]`. The + // `update`/`list`/`info` subcommands are handled CLI-side (main.rs) + // and never reach here — by this point `rest[0]` is a `name/cmd` + // adapter spec. Load it, map the args onto the adapter's declared + // `args`, and emit a `site` action: the daemon navigates to the + // adapter's @meta.domain (reusing the tab if already there) and evals + // the adapter function in the site's own logged-in page. + let spec = rest.first().ok_or(ParseError::InvalidValue { + message: "site requires / (run `chrome-use site list`)".to_string(), + usage: "site / [args]", + })?; + let adapter = + crate::site::load_adapter(spec).map_err(|e| ParseError::InvalidValue { + message: e, + usage: "site / [args]", + })?; + let domain = adapter + .domain() + .ok_or(ParseError::InvalidValue { + message: format!("site: adapter `{spec}` @meta is missing a \"domain\""), + usage: "site /", + })? + .to_string(); + // Split remaining args: `--key value` → named, everything else → positional. + let mut positional: Vec = Vec::new(); + let mut named: Vec<(String, String)> = Vec::new(); + let mut it = rest[1..].iter(); + while let Some(a) = it.next() { + if let Some(key) = a.strip_prefix("--") { + let val = it.next().map(|s| s.to_string()).unwrap_or_default(); + named.push((key.to_string(), val)); + } else { + positional.push(a.to_string()); + } + } + let mapped = crate::site::map_args(&adapter, &positional, &named); + let script = crate::site::build_eval(&adapter, &mapped); + Ok(json!({ "id": id, "action": "site", "domain": domain, "script": script })) + } + // === Stealth self-check === "stealth" => { // `stealth [status]` — local stealth self-check: mode, live probes diff --git a/cli/src/main.rs b/cli/src/main.rs index bb18f4e..e85ef5a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -10,6 +10,7 @@ mod flags; mod install; mod native; mod output; +mod site; mod skills; mod test_runner; #[cfg(test)] @@ -834,6 +835,84 @@ fn main() { exit(test_runner::run_test(suite, &flags)); } + // Handle `site`: site adapters — turn a website into a structured-data CLI by + // running a per-command JS adapter inside your logged-in tab. `update`/`list`/ + // `info` are CLI-side (download/filesystem); `site / [args]` falls + // through to the daemon dispatch below (navigate to the adapter's domain + eval). + if clean.first().map(|s| s.as_str()) == Some("site") { + match clean.get(1).map(|s| s.as_str()) { + Some("update") => { + let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime"); + match rt.block_on(site::update()) { + Ok(n) if flags.json => { + println!("{}", json!({ "success": true, "adapters": n })) + } + Ok(n) => println!( + "{} synced {} site adapters → ~/.chrome-use/sites (run `chrome-use site list`)", + color::success_indicator(), + n + ), + Err(e) => { + eprintln!("{} {}", color::error_indicator(), e); + exit(1); + } + } + return; + } + Some("list") => { + match site::list_adapters() { + Ok(list) if flags.json => { + println!("{}", json!({ "success": true, "adapters": list })) + } + Ok(list) if list.is_empty() => { + println!("no site adapters installed — run `chrome-use site update`") + } + Ok(list) => { + for a in &list { + println!("{a}"); + } + eprintln!( + "{}", + color::dim(&format!( + "{} adapters · run: chrome-use site / [args]", + list.len() + )) + ); + } + Err(e) => { + eprintln!("{} {}", color::error_indicator(), e); + exit(1); + } + } + return; + } + Some("info") => { + let spec = clean.get(2).cloned().unwrap_or_default(); + match site::load_adapter(&spec) { + Ok(a) => println!( + "{}", + serde_json::to_string_pretty(&a.meta).unwrap_or_default() + ), + Err(e) => { + eprintln!("{} {}", color::error_indicator(), e); + exit(1); + } + } + return; + } + // `site / [args]` → fall through to the daemon dispatch. + Some(spec) if spec.contains('/') => {} + _ => { + eprintln!( + "{} usage: chrome-use site / [args] | site update | site list | \ + site info /", + color::error_indicator() + ); + exit(2); + } + } + } + // Handle skills command (doesn't need daemon) if clean.first().map(|s| s.as_str()) == Some("skills") { skills::run_skills(&clean, flags.json); diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index 754d261..f1c5255 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -1315,6 +1315,7 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { "title" => handle_title(state).await, "content" => handle_content(state).await, "evaluate" => handle_evaluate(cmd, state).await, + "site" => handle_site(cmd, state).await, "close" => handle_close(state).await, "stealth_status" => handle_stealth_status(state).await, "snapshot" => handle_snapshot(cmd, state).await, @@ -2698,6 +2699,47 @@ async fn handle_evaluate(cmd: &Value, state: &DaemonState) -> Result Result { + let domain = cmd + .get("domain") + .and_then(|v| v.as_str()) + .ok_or("site: missing 'domain'")? + .to_string(); + let script = cmd + .get("script") + .and_then(|v| v.as_str()) + .ok_or("site: missing 'script'")? + .to_string(); + + let current = match state.browser.as_ref() { + Some(mgr) => mgr.get_url().await.unwrap_or_default(), + None => String::new(), + }; + let on_domain = url::Url::parse(¤t) + .ok() + .and_then(|u| u.host_str().map(|h| h.to_string())) + .map(|h| h == domain || h.ends_with(&format!(".{domain}"))) + .unwrap_or(false); + if !on_domain { + let nav = json!({ "url": format!("https://{domain}/") }); + handle_navigate(&nav, state).await?; + } + + let eval_cmd = json!({ "script": script }); + let out = handle_evaluate(&eval_cmd, state).await?; + Ok(json!({ + "result": out.get("result").cloned().unwrap_or(Value::Null), + "origin": out.get("origin").cloned().unwrap_or(Value::Null), + "domain": domain, + })) +} + /// Local stealth self-check: reports the active mode, live fingerprint probes, /// and the list of applied overrides — so an agent (or human) can confirm /// stealth is working without driving an external detector, and audit exactly diff --git a/cli/src/output.rs b/cli/src/output.rs index 480109d..f39bd07 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -3366,6 +3366,14 @@ Batch: batch [--bail] ["cmd" ...] Execute multiple commands sequentially (args or stdin) --bail stops on first error (default: continue all) +Site adapters: turn a website into a structured-data CLI (runs as you, in your tab) + site update Fetch the community adapter pack into ~/.chrome-use/sites + site list List installed adapters (name/cmd) + site info / Show an adapter's @meta (args, domain, capabilities) + site / [args] Run an adapter: navigate to its site + return JSON + e.g. site github/issues epiral/repo, site reddit/search rust + Positional args fill declared args in order; --key value overrides + Auth Vault: auth save [opts] Save auth profile (--url, --username, --password/--password-stdin) auth login Login using saved credentials (waits for form fields) diff --git a/cli/src/site.rs b/cli/src/site.rs new file mode 100644 index 0000000..d229ed6 --- /dev/null +++ b/cli/src/site.rs @@ -0,0 +1,267 @@ +//! Site adapters: turn any website into a structured-data CLI by running a small +//! per-command JS adapter inside your real, logged-in browser tab (it reuses the +//! site's cookies / same-origin fetch / its own webpack modules — the site thinks +//! it's you, because it is). +//! +//! The adapter format is the community **bb-sites** convention +//! (): one `.js` file per command, a +//! `/* @meta {...} */` JSON header (name, description, domain, args), then an +//! `async function(args){ ... return {...} }`. chrome-use ships none of those +//! adapters — `chrome-use site update` fetches the upstream repo at runtime into +//! `~/.chrome-use/sites` (like a package manager pulling a dependency), so the +//! adapters stay the property of their authors. Running an adapter navigates to +//! its `@meta.domain` and `eval`s the function in the site's own logged-in page. + +use std::path::PathBuf; + +use serde_json::Value; + +const SITES_ZIP_URL: &str = "https://github.com/epiral/bb-sites/archive/refs/heads/main.zip"; + +/// `~/.chrome-use/sites` — where synced adapters live. +pub fn sites_dir() -> Option { + dirs_home().map(|h| h.join(".chrome-use").join("sites")) +} + +fn dirs_home() -> Option { + std::env::var_os("HOME").map(PathBuf::from) +} + +/// Parsed adapter: its `@meta` JSON and the raw `async function(args){...}` source. +pub struct Adapter { + pub meta: Value, + pub func_src: String, +} + +impl Adapter { + pub fn domain(&self) -> Option<&str> { + self.meta.get("domain").and_then(|v| v.as_str()) + } +} + +/// Load `//.js`, splitting the `/* @meta {...} */` header from +/// the function body. `spec` is `name/cmd`. +pub fn load_adapter(spec: &str) -> Result { + let (name, cmd) = spec + .split_once('/') + .ok_or_else(|| format!("site: expected /, got `{spec}`"))?; + if name.is_empty() + || cmd.is_empty() + || name.contains("..") + || cmd.contains("..") + || name.contains('/') + || cmd.contains('/') + { + return Err(format!("site: invalid adapter spec `{spec}`")); + } + let dir = sites_dir().ok_or("site: cannot resolve home dir")?; + let path = dir.join(name).join(format!("{cmd}.js")); + if !path.exists() { + return Err(format!( + "site: adapter `{spec}` not found. Run `chrome-use site update` to sync adapters, \ + or `chrome-use site list` to see what's installed." + )); + } + let raw = std::fs::read_to_string(&path).map_err(|e| format!("site: read {spec}: {e}"))?; + parse_adapter(&raw, spec) +} + +/// Split the `@meta` JSON block and the function source from an adapter file. +pub fn parse_adapter(raw: &str, spec: &str) -> Result { + let start = raw + .find("@meta") + .and_then(|i| raw[i..].find('{').map(|j| i + j)) + .ok_or_else(|| format!("site: {spec} missing /* @meta {{...}} */ header"))?; + // Find the matching close brace for the @meta object (brace-count, string-aware). + let bytes = raw.as_bytes(); + let mut depth = 0i32; + let mut in_str = false; + let mut esc = false; + let mut end = None; + for (k, &b) in bytes.iter().enumerate().skip(start) { + if in_str { + if esc { + esc = false; + } else if b == b'\\' { + esc = true; + } else if b == b'"' { + in_str = false; + } + continue; + } + match b { + b'"' => in_str = true, + b'{' => depth += 1, + b'}' => { + depth -= 1; + if depth == 0 { + end = Some(k + 1); + break; + } + } + _ => {} + } + } + let end = end.ok_or_else(|| format!("site: {spec} @meta header has no closing brace"))?; + let meta: Value = serde_json::from_str(&raw[start..end]) + .map_err(|e| format!("site: {spec} @meta is not valid JSON: {e}"))?; + // The function is everything after the meta comment's closing `*/`. + let after = raw[end..].find("*/").map(|i| end + i + 2).unwrap_or(end); + let func_src = raw[after..].trim().to_string(); + if func_src.is_empty() { + return Err(format!("site: {spec} has no function body after @meta")); + } + Ok(Adapter { meta, func_src }) +} + +/// Build the JS to eval: `()()`. The adapter's +/// `async function(args)` returns a promise; chrome-use's eval awaits it. +pub fn build_eval(adapter: &Adapter, args: &Value) -> String { + let args_json = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string()); + format!("({})({})", adapter.func_src, args_json) +} + +/// List installed adapters as `name/cmd` strings (sorted). +pub fn list_adapters() -> Result, String> { + let dir = sites_dir().ok_or("site: cannot resolve home dir")?; + if !dir.exists() { + return Ok(Vec::new()); + } + let mut out = Vec::new(); + for site in std::fs::read_dir(&dir) + .map_err(|e| e.to_string())? + .flatten() + { + if !site.path().is_dir() { + continue; + } + let name = site.file_name().to_string_lossy().to_string(); + for cmd in std::fs::read_dir(site.path()) + .map_err(|e| e.to_string())? + .flatten() + { + let p = cmd.path(); + if p.extension().and_then(|e| e.to_str()) == Some("js") { + if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) { + out.push(format!("{name}/{stem}")); + } + } + } + } + out.sort(); + Ok(out) +} + +/// Download the bb-sites repo zip and extract its adapters into `~/.chrome-use/sites`. +pub async fn update() -> Result { + let dir = sites_dir().ok_or("site: cannot resolve home dir")?; + let client = reqwest::Client::builder() + .user_agent("chrome-use") + .build() + .map_err(|e| e.to_string())?; + let bytes = client + .get(SITES_ZIP_URL) + .send() + .await + .map_err(|e| format!("site update: download failed: {e}"))? + .error_for_status() + .map_err(|e| format!("site update: {e}"))? + .bytes() + .await + .map_err(|e| format!("site update: read body: {e}"))?; + + let cursor = std::io::Cursor::new(bytes); + let mut zip = zip::ZipArchive::new(cursor).map_err(|e| format!("site update: bad zip: {e}"))?; + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let mut count = 0usize; + for i in 0..zip.len() { + let mut f = zip.by_index(i).map_err(|e| e.to_string())?; + let Some(enclosed) = f.enclosed_name() else { + continue; + }; + // Strip the top-level `bb-sites-main/` component from the archive path. + let rel: PathBuf = enclosed.components().skip(1).collect(); + if rel.as_os_str().is_empty() { + continue; + } + let out = dir.join(&rel); + if f.is_dir() { + let _ = std::fs::create_dir_all(&out); + continue; + } + if let Some(parent) = out.parent() { + let _ = std::fs::create_dir_all(parent); + } + let mut buf = Vec::new(); + std::io::copy(&mut f, &mut buf).map_err(|e| e.to_string())?; + std::fs::write(&out, &buf).map_err(|e| e.to_string())?; + if out.extension().and_then(|e| e.to_str()) == Some("js") { + count += 1; + } + } + Ok(count) +} + +/// Map CLI args to the adapter's `args` object. Positional args fill the adapter's +/// declared `args` keys in order; `--key value` overrides by name. The adapter +/// validates required args itself. +pub fn map_args(adapter: &Adapter, positional: &[String], named: &[(String, String)]) -> Value { + let mut obj = serde_json::Map::new(); + let keys: Vec = adapter + .meta + .get("args") + .and_then(|a| a.as_object()) + .map(|m| m.keys().cloned().collect()) + .unwrap_or_default(); + for (i, val) in positional.iter().enumerate() { + if let Some(k) = keys.get(i) { + obj.insert(k.clone(), Value::String(val.clone())); + } + } + for (k, v) in named { + obj.insert(k.clone(), Value::String(v.clone())); + } + Value::Object(obj) +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = r#"/* @meta +{ + "name": "github/issues", + "domain": "github.com", + "args": { "repo": {"required": true}, "state": {"required": false} } +} +*/ + +async function(args) { return { repo: args.repo }; }"#; + + #[test] + fn parses_meta_and_function() { + let a = parse_adapter(SAMPLE, "github/issues").unwrap(); + assert_eq!(a.domain(), Some("github.com")); + assert!(a.func_src.starts_with("async function(args)")); + } + + #[test] + fn build_eval_wraps_and_passes_args() { + let a = parse_adapter(SAMPLE, "github/issues").unwrap(); + let args = map_args( + &a, + &["owner/repo".into()], + &[("state".into(), "closed".into())], + ); + let js = build_eval(&a, &args); + assert!(js.contains("async function(args)")); + assert!(js.contains("\"repo\":\"owner/repo\"")); + assert!(js.contains("\"state\":\"closed\"")); + } + + #[test] + fn rejects_bad_spec() { + assert!(load_adapter("noslash").is_err()); + assert!(load_adapter("../etc/passwd").is_err()); + } +} diff --git a/package.json b/package.json index b0fd3b9..fb9a2fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chrome-use", - "version": "1.5.18", + "version": "1.5.19", "description": "chrome-use — drive your real, logged-in Chrome from any AI agent, stealth by default", "type": "module", "packageManager": "pnpm@11.1.3", diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md index df6ab93..a4b30f3 100644 --- a/skill-data/core/SKILL.md +++ b/skill-data/core/SKILL.md @@ -68,6 +68,7 @@ need a **real, logged-in browser** — not for reading text off a public page. |---|---| | Discover what exists / find sources | `WebSearch` | | Specific facts from a static or public page | `WebFetch` or `curl` (no browser) | +| **Structured data from a known site** (GitHub issues, Reddit/HN search, Bilibili/Twitter feed, …) — esp. behind login | `chrome-use site /` (see below) — skip snapshot+click entirely | | Login state, interaction, JS-rendered or anti-bot pages | **chrome-use** (this skill) | | A page the user saved before / an internal system | `chrome-use find-url ` (their bookmarks), then open it | | The user's **own already-open, logged-in** Chrome window | the **extension connect** flow (below) | @@ -196,6 +197,27 @@ chrome-use eval "[...document.forms[0].elements].filter(e=>!e.validity.valid).ma chrome-use eval "document.querySelector('#stubborn').click()" # direct DOM click, bypasses overlays ``` +## Site adapters — the cheapest path for "read structured data from site X" + +Before you `open` + `snapshot` + click your way through GitHub/Reddit/Bilibili/etc., +check whether a **site adapter** already exists. An adapter is a community-written JS +function that hits the site's own JSON API *from inside your logged-in tab* and returns +clean structured data — no clicking, no scraping, no screenshots. It's the same idea as +`eval`, packaged per-site. + +```bash +chrome-use site update # one-time: fetch the adapter pack (~145 cmds) +chrome-use site list # what's installed (github/issues, reddit/search, …) +chrome-use site info github/issues # an adapter's args + which domain it runs on +chrome-use site github/issues owner/repo --json # run it → JSON (navigates there for you) +``` + +- Positional args fill the adapter's declared args **in order**; `--key value` overrides by name. +- It navigates to the adapter's domain (reusing the current tab if you're already on it), so + login-gated feeds (`bilibili/feed`, `twitter/...`) work because they run as *you*. +- If no adapter fits, fall back to the normal `snapshot`/`eval` loop. Adapters come from the + [bb-sites](https://github.com/epiral/bb-sites) community pack; chrome-use fetches & runs them. + ## Quickstart ```bash