app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
v2.0.677 (insiders): pup-bridge#125: pup prunes its browser download cache a minute after every boot and on demand (pup_cache_prune, with dryRun): superseded builds, the Node-era headless-shell builds and partial downloads go; the pinned build, the newest complete build when the pinned one is missing, and anything a running process or the default browser uses are kept. Plus everything in 2.0.676.
7 files changed
+82−15
rust/Cargo.lock+12−12@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-pup-bridge"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-browser", "pup-cdp",@@ -655,7 +655,7 @@ dependencies = [ [[package]] name = "pup-browser"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-core", "pup-host",@@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "pup-cdp"-version = "2.0.676"+version = "2.0.677" dependencies = [ "serde_json", "thiserror",@@ -677,7 +677,7 @@ dependencies = [ [[package]] name = "pup-core"-version = "2.0.676"+version = "2.0.677" dependencies = [ "serde", "serde_json",@@ -687,7 +687,7 @@ dependencies = [ [[package]] name = "pup-dashboard"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-cdp", "pup-core",@@ -700,7 +700,7 @@ dependencies = [ [[package]] name = "pup-host"-version = "2.0.676"+version = "2.0.677" dependencies = [ "serde", "serde_json",@@ -709,14 +709,14 @@ dependencies = [ [[package]] name = "pup-host-mac"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-host", ] [[package]] name = "pup-host-win"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-core", "pup-host",@@ -726,7 +726,7 @@ dependencies = [ [[package]] name = "pup-identity"-version = "2.0.676"+version = "2.0.677" dependencies = [ "fontdue", "image",@@ -736,7 +736,7 @@ dependencies = [ [[package]] name = "pup-session"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-core", "serde",@@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "pup-vault"-version = "2.0.676"+version = "2.0.677" dependencies = [ "pup-core", "pup-host",@@ -757,7 +757,7 @@ dependencies = [ [[package]] name = "pup-verbs"-version = "2.0.676"+version = "2.0.677" dependencies = [ "base64", "png 0.17.16",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "host", "host-win", "host-mac", "cdp", "browser", "session", # One version for the whole bridge, in lockstep with src/BRIDGE_VERSION (tools/ship.sh bumps both). [workspace.package]-version = "2.0.676"+version = "2.0.677" edition = "2021" license = "MIT"
rust/bridge/src/main.rs+1−1@@ -118,7 +118,7 @@ fn main() { pup_verbs::highfps::start_loop(c.clone()); pup_verbs::extras::start_identity_audit(c.clone()); pup_verbs::extras::start_profile_janitor(c.clone());- { let c2 = c.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(60)); pup_verbs::identity::sweep_dead_identities(&c2, "boot"); }); }+ { let c2 = c.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(60)); pup_verbs::identity::sweep_dead_identities(&c2, "boot"); let _ = pup_verbs::extras::cache_prune(&c2, false); }); } if let Ok(v) = c.ab.call("bridge_info", json!({}), &Caller::default(), Duration::from_secs(4)) { *c.ad_version.lock().unwrap() = v.get("version").and_then(|x| x.as_str()).map(|s| s.to_string()); } }); }
rust/verbs/assets/verb-catalog.json+20@@ -1903,6 +1903,26 @@ "summary": "Delete LEAKED isolated profile directories \u2014 the iso-* throwaway jars minted by isolated:true opens (and by wikiView:public before 2.0.132, when the public view moved to the server-side ?view=public param) whose window is long gone (they used to pile up, 100s per machine). Only removes an iso-* dir that no live session or session file owns, that no Chrome is holding, and that is >15 min old; durable jars (adom-you, named profiles) are NEVER touched. Runs automatically every 15 min too. dryRun:true previews the list without deleting.", "timeoutSeconds": 20 },+ {+ "name": "pup_cache_prune",+ "summary": "Delete superseded browser builds and leftover partial downloads from pup's browser download cache, keeping the build pup drives and anything in use.",+ "input": {+ "dryRun": "optional bool: report what would go, delete nothing"+ },+ "example": {+ "dryRun": true+ },+ "hint": "pup runs this a minute after every boot (2.0.677). Reports removed (path, MB), kept (with why) and freedMB.",+ "related": [+ "pup_readiness",+ "pup_reap_profiles"+ ],+ "timeoutSeconds": 30,+ "output": {},+ "pitfalls": [],+ "longRunning": false,+ "statusVerb": "pup_readiness"+ }, { "name": "pup_sweep_identities", "summary": "Unregister every pup taskbar identity (registry key and any Start Menu anchor) that no live window holds, and delete stale tile/icon generations.",
rust/verbs/src/extras.rs+45@@ -341,3 +341,48 @@ pub fn restart_browser(ctx: &Ctx, caller: &Caller, args: &Value) -> Value { ok(json!({ "profile": profile, "pid": pid, "killed": killed, "windowsSaved": saved, "tabsSaved": tabs_total, "profileReleasedMs": waited, "restoring": restoring, "_hint": if killed { format!("Browser ended. {} window(s) with {tabs_total} tab(s) are being relaunched from their session files right now (about 8 s per window); poll pup_list_windows until the tab counts match `restoring`, then verbs on them work.", restoring.len()) } else { "Could not end the browser process; end it from Task Manager and pup will recover the windows and their tabs the same way.".to_string() } })) }++/// 2.0.677 (pup-bridge#125): the browser download cache (~/.cache/puppeteer) only ever grew: every build bump+/// kept the one it replaced (~410 MB each), and the Node era left chrome-headless-shell builds and `*-dl`+/// download folders the Rust pup never uses (1.63 GB on arav-rog, 2026-09-22). Kept: the pinned build, the newest+/// complete build when the pinned one is missing (it is what drives an offline box), and any build a running+/// process or the saved default browser points into. Everything else goes. Runs a minute after boot and on demand.+pub fn cache_prune(ctx: &Ctx, dry: bool) -> Value {+ fn size_of(p: &std::path::Path) -> u64 {+ let mut n = 0u64;+ if let Ok(rd) = std::fs::read_dir(p) { for e in rd.flatten() { let q = e.path(); if q.is_dir() { n += size_of(&q); } else if let Ok(m) = e.metadata() { n += m.len(); } } }+ n+ }+ let root = ctx.host.cft_cache_dir();+ let norm = |p: &std::path::Path| p.to_string_lossy().replace('\\', "/").to_lowercase();+ // what must survive: running browser executables and the saved default+ let mut keep_paths: Vec<String> = ctx.host.run_shell_script("Get-CimInstance Win32_Process -Filter \"Name='chrome.exe' OR Name='chrome-headless-shell.exe'\" | ForEach-Object { $_.ExecutablePath }", Duration::from_secs(20))+ .lines().map(|l| l.trim().replace('\\', "/").to_lowercase()).filter(|l| !l.is_empty()).collect();+ if let Some(d) = pup_browser::read_default(&*ctx.host) { if let Some(p) = d.get("executablePath").and_then(|v| v.as_str()) { keep_paths.push(p.replace('\\', "/").to_lowercase()); } }+ let pinned = pup_browser::cft_versioned_dir(&*ctx.host, pup_browser::CFT_BUILD_ID);+ let pinned_ok = pup_browser::cft_exe_path(&*ctx.host, pup_browser::CFT_BUILD_ID).is_file();+ let fallback = if pinned_ok { None } else { pup_browser::detect_cft(&*ctx.host).and_then(|c| c.executable_path.ancestors().find(|a| a.parent().map(|pp| pp.ends_with("chrome")).unwrap_or(false)).map(|a| a.to_path_buf())) };+ let mut removed = Vec::new(); let mut kept = Vec::new(); let mut freed = 0u64; let mut failed = Vec::new();+ let Ok(top) = std::fs::read_dir(&root) else { return ok(json!({ "success": true, "cacheDir": norm(&root), "removed": [], "freedMB": 0, "_hint": "No browser download cache on this box." })) };+ for product in top.flatten() {+ let pname = product.file_name().to_string_lossy().to_string();+ let ppath = product.path();+ if !ppath.is_dir() { continue; }+ let builds: Vec<std::path::PathBuf> = if pname == "chrome" { std::fs::read_dir(&ppath).map(|r| r.flatten().map(|e| e.path()).filter(|p| p.is_dir()).collect()).unwrap_or_default() } else { vec![ppath.clone()] };+ for b in builds {+ let bn = norm(&b);+ let in_use = keep_paths.iter().any(|k| k.starts_with(&(bn.clone() + "/")));+ let is_keep = pname == "chrome" && (b == pinned && pinned_ok || fallback.as_ref() == Some(&b));+ let label = b.strip_prefix(&root).map(|x| x.to_string_lossy().replace('\\', "/")).unwrap_or_else(|_| bn.clone());+ if is_keep || in_use { kept.push(json!({ "path": label, "why": if in_use { "a running process or the default browser uses it" } else if b == pinned { "the pinned build" } else { "the newest complete build (the pinned one is missing)" } })); continue; }+ let sz = size_of(&b);+ if dry { removed.push(json!({ "path": label, "mb": sz / 1_048_576 })); freed += sz; continue; }+ match std::fs::remove_dir_all(&b) { Ok(_) => { removed.push(json!({ "path": label, "mb": sz / 1_048_576 })); freed += sz; } Err(e) => failed.push(json!({ "path": label, "error": e.to_string() })) }+ }+ if pname != "chrome" { continue; }+ }+ let mb = freed / 1_048_576;+ if !dry && (!removed.is_empty() || !failed.is_empty()) { pup_core::log(format!("[cache] pruned the browser download cache: {} entr{} removed, {mb} MB freed{}", removed.len(), if removed.len() == 1 { "y" } else { "ies" }, if failed.is_empty() { String::new() } else { format!(", {} could not be removed", failed.len()) })); }+ ok(json!({ "success": true, "dryRun": dry, "cacheDir": norm(&root), "removed": removed, "kept": kept, "failed": failed, "freedMB": mb,+ "_hint": format!("{} {} MB of superseded browser builds and leftover downloads{}. Kept the build pup drives and anything in use. pup runs this a minute after every boot (2.0.677).", if dry { "Would free" } else { "Freed" }, mb, if dry { " (dry run: pass dryRun:false to delete)" } else { "" }) }))+}
rust/verbs/src/lib.rs+2−1@@ -566,7 +566,7 @@ pub const PHASE1_VERBS: &[&str] = &["pup_describe", "pup_readiness", "pup_prewar "pup_raise_os_window", "pup_focus_window", "pup_lower_os_window", "pup_alert_window", "pup_progress", "pup_configure", "pup_use", "pup_clear_cookies", "pup_clear_site_data", "pup_quit_idle_browsers", "pup_rescan", "pup_restart_browser", "pup_set_identity", "pup_audit_icons", "pup_dashboard", "pup_credentials", "credential_list", "credential_set", "credential_delete", "pup_login", "pup_google_accounts", "pup_google_signin", "pup_credential_reveal", "pup_card_set", "pup_cards", "pup_card_delete", "pup_fill_card", "pup_wiki_set_view", "pup_annotate", "pup_floaty_click", "pup_record_start", "pup_record_stop", "pup_record_status", "pup_record_list", "pup_recorder_open", "pup_recorder_close", "pup_screen_record_start", "pup_screen_record_stop", "pup_screen_record_status", "pup_screen_record_list", "pup_highfps_extend",- "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_sweep_identities", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"];+ "pup_screenshot_element", "pup_screenshot_full_res", "pup_download", "pup_launch", "pup_deps", "pup_install_toolbar", "pup_reap_profiles", "pup_cache_prune", "pup_sweep_identities", "pup_uninstall", "pup_move_to_workspace", "pup_assisted_click", "pup_import_scan", "pup_import_browser_logins", "pup_vault_sync", "pup_vault_export", "pup_vault_import"]; pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value { let t0 = Instant::now();@@ -666,6 +666,7 @@ pub fn dispatch(ctx: &Ctx, caller: &Caller, verb: &str, args: &Value) -> Value { "pup_deps" => extras::deps(ctx), "pup_install_toolbar" => extras::install_toolbar(ctx), "pup_reap_profiles" => extras::reap_profiles(ctx, args),+ "pup_cache_prune" => extras::cache_prune(ctx, args.get("dryRun").and_then(|v| v.as_bool()).unwrap_or(false)), "pup_sweep_identities" => { let r = identity::sweep_dead_identities(ctx, "verb"); ok(pup_core::merge(r, json!({ "_hint": "Unregistered every pup taskbar identity (its registry key and any Start Menu anchor) that no live window holds, and removed icon generations nothing references. pup runs this a minute after every boot and releases each window's identity on close (2.0.666)." }))) }, "pup_uninstall" => extras::uninstall(ctx, args), "pup_move_to_workspace" => extras::move_to_workspace(ctx, caller, args),
tools/gen-assets.py+1@@ -143,6 +143,7 @@ _STUBS = { 'pup_quit_idle_browsers': lambda: _entry('pup_quit_idle_browsers', 'Quit every pup browser that has no windows (so the next open loads freshly staged extensions).', {}, {}, 'A browser with live windows is never touched.', ['pup_restart_browser']), 'pup_highfps_extend': lambda: _entry('pup_highfps_extend', 'Renew the highFps lease on a window that must keep rendering while in the background.', {'minutes': 'int, max 120', 'window': "optional: the pup window (default the calling thread's)"}, {'minutes': 20}, 'The lease auto-expires so a background window can never drain the battery; renew only while you still need it.', ['pup_open_window']), 'pup_audit_icons': lambda: _entry('pup_audit_icons', "Read back each pup window's taskbar identity from Windows and compare it with what pup stamped.", {}, {}, 'Detector only: nothing is re-stamped.', ['pup_set_identity', 'pup_sweep_identities']),+ 'pup_cache_prune': lambda: _entry('pup_cache_prune', "Delete superseded browser builds and leftover partial downloads from pup's browser download cache, keeping the build pup drives and anything in use.", {'dryRun': 'optional bool: report what would go, delete nothing'}, {'dryRun': True}, 'pup runs this a minute after every boot (2.0.677). Reports removed (path, MB), kept (with why) and freedMB.', ['pup_readiness', 'pup_reap_profiles']), 'pup_sweep_identities': lambda: _entry('pup_sweep_identities', 'Unregister every pup taskbar identity (registry key and any Start Menu anchor) that no live window holds, and delete stale tile/icon generations.', {}, {}, 'pup runs this a minute after every boot and releases a window identity on close (2.0.666+); on demand it is for a box that ran older builds. Returns unregistered, shortcutsRemoved, iconDirsRemoved.', ['pup_audit_icons']), 'credential_list': lambda: dict(_entry('credential_list', '', {}, {}, '', ['pup_credentials'], like='pup_credentials'), summary='Alias of pup_credentials: list saved logins (host and username only, never passwords).'), 'credential_set': lambda: _entry('credential_set', "Store a login in pup's vault for a host.", {'host': 'str', 'username': 'str', 'password': 'str (never echoed or logged)'}, {'host': 'example.com', 'username': '[email protected]', 'password': '<secret>'}, 'Matched across the site\'s whole registrable domain on return visits.', ['pup_credentials', 'credential_delete', 'pup_login']),