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
Q4: background login-form autofill with no Chrome floaty menu and no foregrounding. The vault already handled the NATIVE Basic-Auth dialog (page.authenticate); this adds the common case John asked about — HTML <form> logins, where Chrome's password manager would pop a native autofill dropdown that pup cannot click without stealing focus. Because pup owns the credential, it skips Chrome's UI entirely: new credential_vault.fillLoginForm finds the username + password fields (autocomplete=username / type=email / nearest visible text input before the password field) and fills them over CDP using the native value setter plus bubbled input/change events (React/Vue-safe), optionally submitting via the form's submit button or requestSubmit. New browser_login verb {sessionId, submit?, username?, password?, save?, host?} fills from the vault (matched on the tab's current URL) or from explicit creds, and with save:true stores the explicit cred so next time is automatic. Passwords are never echoed back (only the username is returned for confirmation). autofillFormFromVault is exported for a future auto-on-navigation hook. Pairs with the durable 'you' profile: sign in once and the login persists, and pup can re-fill in the background thereafter.
5 files changed
+121−6
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.123",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.123/adom-bridge-puppeteer-v1.9.123.zip",- "sha256": "1e9f3473a1698833361747d65c6acecdf9068cc144d56301ff1c61c7b3cee914",- "size": 389415,+ "version": "1.9.124",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.124/adom-bridge-puppeteer-v1.9.124.zip",+ "sha256": "2c08904d9ff5185c29c3dcee8f1838e69e6714449ac1c9659dcb2fd7edbdfed9",+ "size": 391726, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.123+1.9.124
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.123",+ "version": "1.9.124", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/credential_vault.js+65@@ -190,12 +190,77 @@ async function applyCredentialsToPage(page, url) { } } +// ── HTML login FORM autofill (v1.9.124) ─────────────────────────────────+// applyCredentialsToPage above handles the NATIVE Basic-Auth dialog. This+// handles the other, more common case John asked about: an HTML <form> login,+// where Chrome's own password manager would pop the floaty autofill dropdown —+// a NATIVE popup pup can't click without foregrounding the window. Because pup+// owns the creds, we skip Chrome's UI entirely: find the fields and fill them+// over CDP, in the background, no dropdown, no focus steal. React/Vue-safe+// (native value setter + bubbled input/change events, the standard technique).+//+// Runs entirely in the page; returns { ok, filledUsername, filledPassword,+// submitted, usernameValue } and NEVER returns the password.+async function fillLoginForm(page, { username, password, submit = false } = {}) {+ if (!password) return { ok: false, reason: 'no_password_supplied' };+ const result = await page.evaluate((USERNAME, PASSWORD, SUBMIT) => {+ const isVisible = (el) => !!el && !el.disabled && el.offsetParent !== null && el.getClientRects().length > 0;+ const pwd = [...document.querySelectorAll('input[type="password"]')].find(isVisible);+ if (!pwd) return { ok: false, reason: 'no_password_field' };+ // Username: prefer autocomplete=username, then email, else the last visible+ // text-ish input that appears BEFORE the password field in DOM order.+ const allInputs = [...document.querySelectorAll('input')];+ const pi = allInputs.indexOf(pwd);+ let user = [...document.querySelectorAll('input[autocomplete="username"], input[type="email"]')].find(isVisible);+ if (!user) {+ const cands = [...document.querySelectorAll('input[type="text"], input[type="tel"], input:not([type])')].filter(isVisible);+ user = [...cands].reverse().find((c) => allInputs.indexOf(c) < pi) || cands[0] || null;+ }+ const setVal = (el, val) => {+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;+ const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;+ try { el.focus(); } catch (e) {}+ setter.call(el, val);+ el.dispatchEvent(new Event('input', { bubbles: true }));+ el.dispatchEvent(new Event('change', { bubbles: true }));+ };+ if (user && USERNAME) setVal(user, USERNAME);+ setVal(pwd, PASSWORD);+ let submitted = false;+ if (SUBMIT) {+ const form = pwd.form;+ const btn = form && [...form.querySelectorAll('button[type="submit"], input[type="submit"], button')].find(isVisible);+ if (btn) { btn.click(); submitted = true; }+ else if (form) { (form.requestSubmit ? form.requestSubmit() : form.submit()); submitted = true; }+ }+ return { ok: true, filledUsername: !!(user && USERNAME), filledPassword: true, submitted, usernameValue: user ? user.value : null };+ }, username || '', password, !!submit);+ return result;+}++// Pull a stored cred for the page's URL and fill its login form in the+// background. No-op (returns null) if no vault match or no login form.+async function autofillFormFromVault(page, url, { submit = false } = {}) {+ const creds = await getCredentialForUrl(url);+ if (!creds) return null;+ try {+ const r = await fillLoginForm(page, { username: creds.username, password: creds.password, submit });+ if (r && r.ok) return { host: creds.host, username: creds.username, submitted: !!r.submitted };+ return null;+ } catch (e) {+ console.log(`[credential_vault] autofillFormFromVault failed for ${creds.host}: ${e.message}`);+ return null;+ }+}+ module.exports = { setCredential, getCredentialForUrl, listCredentials, deleteCredential, applyCredentialsToPage,+ fillLoginForm,+ autofillFormFromVault, // Exposed for tests + diagnostics hostMatchesPattern, scorePattern,
src/server.js+50@@ -9024,6 +9024,56 @@ const server = http.createServer(async (req, res) => { } return; }+ // v1.9.124 — fill (and optionally submit) an HTML login FORM in the BACKGROUND, from the+ // vault or explicit creds, with NO Chrome autofill dropdown and NO foregrounding. This is+ // the answer to Q4: pup owns the creds, so it fills the fields over CDP directly instead of+ // waiting on Chrome's native floaty menu (which pup can't click without stealing focus).+ case 'browser_login': {+ let session, resolvedId;+ try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); }+ catch (e) { sendSessionResolveError(res, e); return; }+ let tab, tabId;+ try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); }+ catch (e) { sendTabResolveError(res, e); return; }+ try {+ const curUrl = tab.page.url();+ let username = args.username, password = args.password, source = 'explicit';+ // Optionally persist an explicit cred so NEXT time it's automatic.+ if (username && password && args.save) {+ let host = args.host;+ try { if (!host) host = new URL(curUrl).hostname; } catch (e) {}+ if (host) { try { await credentialVault.setCredential({ host, username, password }); } catch (e) {} }+ }+ // No explicit creds → pull from the vault by the page's current URL.+ if (!password) {+ const creds = await credentialVault.getCredentialForUrl(curUrl);+ if (creds) { username = creds.username; password = creds.password; source = 'vault'; }+ }+ if (!password) {+ sendJSON(res, { success: false, errorCode: 'no_credential', output: JSON.stringify({ ok: false, url: curUrl }),+ _hint: `No credential to fill. Either pass {username, password} (add {save:true} to remember it for this host), or store one first with credential_set {host:"${(() => { try { return new URL(curUrl).hostname; } catch (e) { return '<host>'; } })()}", username, password} and call browser_login again. pup then fills the form in the BACKGROUND — no Chrome dropdown, no foregrounding.` });+ return;+ }+ const r = await credentialVault.fillLoginForm(tab.page, { username, password, submit: !!args.submit });+ if (!r || !r.ok) {+ sendJSON(res, { success: false, errorCode: r && r.reason === 'no_password_field' ? 'no_login_form' : 'login_fill_failed',+ output: JSON.stringify({ ok: false, reason: r && r.reason, url: curUrl }),+ _hint: (r && r.reason === 'no_password_field')+ ? 'No password field found on this page — it may not be the login page yet, or the form is in an iframe / shadow DOM pup did not reach. Navigate to the actual login page first, then retry.'+ : 'Could not fill the login form. Screenshot the page to see its state.' });+ return;+ }+ sendJSON(res, { success: true, output: JSON.stringify({+ ok: true, url: curUrl, credentialSource: source,+ filledUsername: r.filledUsername, filledPassword: r.filledPassword,+ submitted: r.submitted, usernameFilled: r.usernameValue, // username only; password is NEVER returned+ }),+ _hint: `Login form filled in the BACKGROUND from ${source} creds${r.submitted ? ' and SUBMITTED' : ' (not submitted — pass {submit:true} to submit, or drive the submit button yourself)'}. No Chrome autofill dropdown was involved and the window was never foregrounded. Verify with browser_screenshot before assuming you are signed in.` });+ } catch (e) {+ sendJSON(res, { success: false, error: e.message });+ }+ return;+ } // ─── Desktop recording (HUD) ─────────────────────────────────── case 'desktop_recorder_open': {