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.
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/Cargo.lock +1 -1
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/Cargo.toml +1 -1
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/PKG-INFO +2 -1
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/README.md +1 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/SKILL.md +2 -1
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/install.sh +1 -1
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/pyproject.toml +1 -1
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/main.rs +18 -5
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/session.rs +29 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/.gitignore +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/actions.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/cdp.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/chrome.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/daemon_main.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/install.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/js.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/lib.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/server.rs +0 -0
- {browser_automation_cli-0.7.0 → browser_automation_cli-0.7.2}/src/update.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: browser-automation-cli
|
|
3
|
-
Version: 0.7.
|
|
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
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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": "
|
|
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() };
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|