browser-automation-cli 0.7.0__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/Cargo.lock +1 -1
  2. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/Cargo.toml +1 -1
  3. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/PKG-INFO +2 -1
  4. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/README.md +1 -0
  5. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/SKILL.md +2 -1
  6. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/install.sh +1 -1
  7. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/pyproject.toml +1 -1
  8. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/main.rs +18 -5
  9. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/session.rs +29 -0
  10. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/.gitignore +0 -0
  11. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/actions.rs +0 -0
  12. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/cdp.rs +0 -0
  13. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/chrome.rs +0 -0
  14. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/daemon_main.rs +0 -0
  15. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/install.rs +0 -0
  16. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/js.rs +0 -0
  17. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/lib.rs +0 -0
  18. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/server.rs +0 -0
  19. {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/update.rs +0 -0
@@ -34,7 +34,7 @@ dependencies = [
34
34
 
35
35
  [[package]]
36
36
  name = "browser-cli"
37
- version = "0.7.0"
37
+ version = "0.7.2"
38
38
  dependencies = [
39
39
  "base64",
40
40
  "futures-util",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "browser-cli"
3
- version = "0.7.0"
3
+ version = "0.7.2"
4
4
  edition = "2021"
5
5
  description = "Browser automation daemon + CLI for coding agents. Persistent sessions, no MCP, no extensions."
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: browser-automation-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Operating System :: MacOS
@@ -116,6 +116,7 @@ browser profile delete <name> # delete a profile + its logins (ea
116
116
  browser profile ephemeral # make throwaway the default for new sessions
117
117
  browser create --profile <name> # per-session: this session uses its own persistent login
118
118
  browser create --ephemeral # per-session: throwaway, isolated
119
+ browser <id> show | hide # flip a profile visible<->headless seamlessly (sessions kept)
119
120
  # (different --profile = concurrent & isolated; same = shared tabs)
120
121
  browser cleanup # kill Chromium processes launched from Playwright's cache
121
122
  browser install skill # install the agent skill into Claude Code / Codex / OpenCode
@@ -101,6 +101,7 @@ browser profile delete <name> # delete a profile + its logins (ea
101
101
  browser profile ephemeral # make throwaway the default for new sessions
102
102
  browser create --profile <name> # per-session: this session uses its own persistent login
103
103
  browser create --ephemeral # per-session: throwaway, isolated
104
+ browser <id> show | hide # flip a profile visible<->headless seamlessly (sessions kept)
104
105
  # (different --profile = concurrent & isolated; same = shared tabs)
105
106
  browser cleanup # kill Chromium processes launched from Playwright's cache
106
107
  browser install skill # install the agent skill into Claude Code / Codex / OpenCode
@@ -50,10 +50,11 @@ If a site needs login: `browser <id> show` → ask the user to log in in the win
50
50
 
51
51
  ## Profiles (persistent logins)
52
52
 
53
- - **Default is a persistent profile named `default`.** The first `create` opens a visible window so the user signs in once; every later session — including headless ones after a `browser shutdown` — reuses that authenticated profile. Do not ask for credentials; the user logs in in the window.
53
+ - **Default is a persistent profile named `default`.** The first `create` opens a visible window so the user signs in once; every later session reuses that authenticated profile. Do not ask for credentials; the user logs in in the window.
54
54
  - `browser profile status` — show the active profile and list existing ones.
55
55
  - `browser profile <name>` / `browser profile new <name>` — switch to / create a named persistent profile (e.g. a second account). Restart the daemon (`browser shutdown`) to apply.
56
56
  - `browser profile ephemeral` — make throwaway the default for new sessions.
57
+ - **Show/hide is seamless:** `browser <id> show` / `hide` flips a persistent profile between a visible window and headless without closing sessions (they stay, tabs reload, login persists).
57
58
  - **Per session:** `browser create --profile <name>` gives that session its own persistent login; `--profile` differs across sessions run them concurrently and isolated (each its own Chrome), while the same `--profile` shares one login across sessions (separate tabs). `browser create --ephemeral` is a throwaway isolated session.
58
59
  - **`browser profile delete <name>` — delete a profile when it is no longer needed.** Each profile is a full Chrome profile on disk (~100 MB and growing with cache/history), so remove ones you do not need to reclaim space. This erases that profile's logins.
59
60
 
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
  # Install the Rust browser/browser-daemon binaries from the GitHub release into ~/.local/bin.
3
3
  set -e
4
- V="${BROWSER_CLI_VERSION:-0.7.0}"; OS=$(uname -s | tr A-Z a-z); ARCH=$(uname -m)
4
+ V="${BROWSER_CLI_VERSION:-0.7.2}"; OS=$(uname -s | tr A-Z a-z); ARCH=$(uname -m)
5
5
  URL="https://github.com/jshan9078/browser-automation-cli/releases/download/v$V/browser-cli-$V-$OS-$ARCH.tar.gz"
6
6
  DEST="${BROWSER_CLI_BIN:-$HOME/.local/bin}"; mkdir -p "$DEST"
7
7
  echo "Downloading $URL"; curl -fsSL "$URL" | tar -C "$DEST" -xzf -
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "browser-automation-cli"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "Browser automation daemon + CLI for coding agents. Persistent sessions, no MCP, no extensions."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -17,7 +17,8 @@ Standalone (no daemon):
17
17
  browser cleanup Kill Chromium processes started by this tool
18
18
  browser --version | update Show version / upgrade (daily PyPI check; BROWSER_NO_UPDATE_CHECK=1 disables)
19
19
  browser install skill [target...] Install the agent skill (SKILL.md) into Claude Code / Codex /
20
- OpenCode skill directories (all detected when no target given)
20
+ OpenCode skill directories (all detected when no target given).
21
+ `browser update` auto-refreshes already-installed skills.
21
22
  browser engine [auto|managed|system|<path>]
22
23
  Which browser to launch: managed = pinned Chrome for Testing,
23
24
  system = your installed Chrome/Edge/Brave/Chromium (no download),
@@ -349,7 +350,15 @@ fn update_cmd() -> i32 {
349
350
  if browser_cli::update::is_newer(latest, browser_cli::update::CURRENT) {
350
351
  println!("{latest} available (you have {}). Upgrading with: uv tool upgrade browser-automation-cli", browser_cli::update::CURRENT);
351
352
  let st = Command::new("uv").args(["tool", "upgrade", "browser-automation-cli"]).status();
352
- match st { Ok(s) if s.success() => { println!("Restart the daemon to use the new version: browser shutdown && browser-daemon &"); 0 } _ => { eprintln!("Upgrade failed; run it manually: uv tool upgrade browser-automation-cli (or pip install -U browser-automation-cli)"); 1 } }
353
+ match st {
354
+ Ok(s) if s.success() => {
355
+ // refresh already-installed agent skills using the upgraded binary (embeds the new SKILL.md)
356
+ if let Ok(exe) = std::env::current_exe() { let _ = Command::new(exe).args(["install", "skill", "--existing"]).status(); }
357
+ println!("Restart the daemon to use the new version: browser shutdown && browser-daemon &");
358
+ 0
359
+ }
360
+ _ => { eprintln!("Upgrade failed; run it manually: uv tool upgrade browser-automation-cli (or pip install -U browser-automation-cli)"); 1 }
361
+ }
353
362
  } else { println!("Up to date ({}).", browser_cli::update::CURRENT); 0 }
354
363
  }
355
364
 
@@ -381,11 +390,15 @@ fn install_skill(args: &[String]) -> i32 {
381
390
  ("codex", home.join(".codex"), home.join(".codex/skills")),
382
391
  ("opencode", home.join(".config/opencode"), home.join(".config/opencode/skills")),
383
392
  ];
393
+ let existing_only = args.iter().any(|a| a == "--existing");
384
394
  let mut targets: Vec<(String, std::path::PathBuf)> = Vec::new();
385
- if args.is_empty() {
395
+ if args.is_empty() || (existing_only && args.len() == 1) {
386
396
  for (name, marker, dir) in known {
387
- if marker.exists() { targets.push((name.to_string(), dir.clone())); }
397
+ // --existing: only agents that already have our skill; else any agent that is installed
398
+ let take = if existing_only { dir.join("browser-cli/SKILL.md").exists() } else { marker.exists() };
399
+ if take { targets.push((name.to_string(), dir.clone())); }
388
400
  }
401
+ if existing_only && targets.is_empty() { return 0; } // nothing previously installed; quietly do nothing
389
402
  if targets.is_empty() {
390
403
  eprintln!("No agent directories found (~/.claude, ~/.codex, ~/.config/opencode).");
391
404
  eprintln!("Pick one explicitly: browser install skill claude|codex|opencode, or pass a skills directory path.");
@@ -434,7 +447,7 @@ fn profile_cmd(args: &[String]) -> i32 {
434
447
  let mut cfg = read_cfg(); cfg["profile"] = json!(name); cfg.as_object_mut().map(|o| o.remove("ephemeral"));
435
448
  if let Err(e) = write_cfg(&cfg) { eprintln!("{e}"); return 1; }
436
449
  println!("{}", serde_json::to_string_pretty(&json!({"profile": name, "dir": dir.to_string_lossy(),
437
- "note": "restart the daemon to apply (browser shutdown). First `create` opens a window to sign in; later sessions reuse it."})).unwrap());
450
+ "note": "first `create` opens a window to sign in; later sessions reuse it. `browser <id> show`/`hide` flips visible/headless without closing sessions."})).unwrap());
438
451
  0
439
452
  };
440
453
  match args.first().map(String::as_str) {
@@ -226,6 +226,25 @@ impl Manager {
226
226
  }
227
227
  }
228
228
 
229
+ /// Seamlessly switch a persistent profile between headed and headless. A profile dir can only be
230
+ /// held by one Chrome process, so this relaunches that profile's browser in the new mode and
231
+ /// re-attaches ALL of the profile's live sessions (same session IDs, tabs restored to their URLs;
232
+ /// login persists on disk). Open pages reload — no session is closed. No-op if already in `headless`.
233
+ pub async fn switch_profile_mode(&mut self, profile: &str, headless: bool) -> Result<(), String> {
234
+ let key = browser_key(Some(profile), headless);
235
+ let running_mode = self.browsers.get(&key).filter(|b| b.cdp.is_alive()).map(|b| b.headless);
236
+ if running_mode == Some(headless) || running_mode.is_none() { return Ok(()); } // already right, or not running
237
+ // live sessions of this profile, in a stable order
238
+ let mut live_ids: Vec<String> = self.sessions.iter().filter(|(_, s)| s.profile.as_deref() == Some(profile) && s.live.is_some()).map(|(id, _)| id.clone()).collect();
239
+ live_ids.sort();
240
+ for id in &live_ids { self.save(id).await; } // persist current URL to restore
241
+ for id in &live_ids { let s = self.sessions.get_mut(id).unwrap(); s.frozen = false; s.live = None; } // drop dead handles (default context, nothing to dispose)
242
+ if let Some(mut b) = self.browsers.remove(&key) { b.close().await; } // close old-mode browser (flushes cookies)
243
+ for id in &live_ids { self.attach(id, !headless, None).await?; } // relaunch (new mode) + re-attach each session
244
+ eprintln!("[daemon] switched profile '{profile}' to {} ({} session(s) re-attached)", if headless { "headless" } else { "headed" }, live_ids.len());
245
+ Ok(())
246
+ }
247
+
229
248
  pub async fn create(&mut self, profile: Option<String>, visible: bool) -> Result<String, String> {
230
249
  // First use of a persistent profile opens a visible window so the user can sign in;
231
250
  // every later session reuses that authenticated profile (headless by default).
@@ -234,6 +253,8 @@ impl Manager {
234
253
  if let Some(name) = &profile {
235
254
  if !visible && !chrome::profile_dir(name).join("Default").exists() { visible = true; first_use = true; }
236
255
  }
256
+ // If this profile is already running in the other mode, switch it seamlessly rather than erroring.
257
+ if let Some(name) = &profile { self.switch_profile_mode(name, !visible).await?; }
237
258
  let id = uuid::Uuid::new_v4().simple().to_string()[..8].to_string();
238
259
  self.sessions.insert(id.clone(), Session { id: id.clone(), profile: profile.clone(), visible, live: None, created_at: now(), last_used: Instant::now(), frozen: false, busy: 0, title: String::new(), saved_url: "about:blank".into(), saved_state: None, console: VecDeque::new() });
239
260
  if let Err(e) = self.attach(&id, visible, None).await { self.sessions.remove(&id); return Err(e); }
@@ -257,6 +278,14 @@ impl Manager {
257
278
 
258
279
  pub async fn set_visible(&mut self, id: &str, visible: bool) -> Result<(), String> {
259
280
  if !self.sessions.contains_key(id) { return Err("Session not found".into()); }
281
+ // Persistent profile: show/hide flips the whole profile's browser (all its sessions share one
282
+ // process), done seamlessly — sessions stay, tabs restore, no manual close.
283
+ if let Some(name) = self.sessions[id].profile.clone() {
284
+ self.switch_profile_mode(&name, !visible).await?;
285
+ if let Some(s) = self.sessions.get_mut(id) { s.last_used = Instant::now(); }
286
+ self.close_idle_browsers().await;
287
+ return Ok(());
288
+ }
260
289
  let s = &self.sessions[id];
261
290
  if s.live.is_some() && s.visible == visible { return Ok(()); }
262
291
  let url = if let Some(l) = &s.live { l.page.url().await } else { s.saved_url.clone() };