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
Zero synchronous PowerShell: all 7 remaining execFileSync window-management helpers (idle probe, foreground reads, force-activate, the three backgrounding paths, z-bottom, flash-clear) are now async with every call site awaited, so pup's event loop can never again stall on a powershell spawn and starve ab's health probe (the ab61 reap trigger); the sync runner carries a do-not-add-callers tombstone until ab ships native window verbs and PowerShell is deleted outright
4 files changed
+40−37
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.22",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.22/adom-bridge-puppeteer-v2.0.22.zip",- "sha256": "b520c78dea0ccfef153eb976ba21caaa37ceb48e7bf085682e0a2949966acc1a",- "size": 646811,+ "version": "2.0.23",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.23/adom-bridge-puppeteer-v2.0.23.zip",+ "sha256": "0063486be0bccfdfb8d1be736bafdc9f146d5d50d55f2430ea82f9cc0bb2fc11",+ "size": 646962, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.22+2.0.23
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.22",+ "version": "2.0.23", "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. pup_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. pup_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, pup_alert_window posts a native toast whose click focuses the window, pup_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-bridge", "author": "Adom Inc.",
src/server.js+34−31@@ -43,6 +43,9 @@ console.log = (...args) => { // you just burn a doomed process spawn on every call, forever. Guarding the primitive // instead of trusting every future caller is the same defense-in-depth as making a // module import-safe under the wrong interpreter: you don't own who calls you next.+// v2.0.23 (John): DO NOT ADD CALLERS. Synchronous PowerShell blocks the event loop (starved ab's+// health probe, got live bridges reaped — ab#61). All 7 former callers are async now; this stays+// only until ab ships the native window verbs and pup's PowerShell is deleted outright. function runPsHidden(scriptPath, { timeout = 6000 } = {}) { if (process.platform !== 'win32') return ''; return execFileSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', scriptPath], { encoding: 'utf8', timeout, windowsHide: true });@@ -2156,7 +2159,7 @@ async function ensureSafeForNativeInput(sessionId, { reason = 'drive the window console.log(`[native-guard] "${sessionId}": no hwnd — REFUSING native input (cannot prove where it would land)`); return false; }- const fg = osGetForegroundWindow();+ const fg = await osGetForegroundWindow(); if (String(fg) === String(hwnd)) return true; // already ours; safe to dispatch if (!warn) {@@ -2174,7 +2177,7 @@ async function ensureSafeForNativeInput(sessionId, { reason = 'drive the window return false; } // Re-verify at the last possible moment.- const fg2 = osGetForegroundWindow();+ const fg2 = await osGetForegroundWindow(); if (String(fg2) !== String(hwnd)) { console.log(`[native-guard] "${sessionId}": focus moved again at dispatch time — REFUSED native input`); return false;@@ -2228,7 +2231,7 @@ async function waitForUserReady(sessionId, hwnd, { maxMs = 120000 } = {}) { const _capMs = Math.max(500, Math.min(5000, Number(pupSettings().fgCaptionMs) || 2000)); const MAX_CAPS = 3; // initial reminder + at most 2 more, then stop while (Date.now() - start < maxMs) {- const fg = osGetForegroundWindow();+ const fg = await osGetForegroundWindow(); if (fg === hwnd) { if (!stableSince) stableSince = Date.now(); if (Date.now() - stableSince >= 1200) return true;@@ -2775,7 +2778,7 @@ function osSendWindowToBack(profileName) { // v1.8.34: capture the user's CURRENT foreground window (decimal HWND string) right // before we launch a pup browser, so the backgrounder can hand keyboard focus back to it.-function osIdleMs() {+async function osIdleMs() { // v1.9.292 (John: differentiate HIS taskbar click from a JS/Chrome raise). Windows records the // time of the last user input (keyboard/mouse). A taskbar click that raised a window = very recent // input; a programmatic raise = none. Returns ms since the last input, or null if unknown.@@ -2795,12 +2798,12 @@ function osIdleMs() { 'Write-Output ([WinIdle]::Idle())', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- const out = (runPsHidden(scriptPath, { timeout: 4000 }) || '').trim();+ const out = (await runPsHiddenPromise(scriptPath, { timeout: 4000 }) || '').trim(); const ms = Number(out); return Number.isFinite(ms) && ms >= 0 ? ms : null; } catch (e) { return null; } }-function osGetForegroundWindow() {+async function osGetForegroundWindow() { if (process.platform !== 'win32') return null; try { const scriptPath = path.join(require('os').tmpdir(), `_adom_fg_${process.pid}.ps1`);@@ -2812,7 +2815,7 @@ function osGetForegroundWindow() { 'Write-Output ([int64][WinFgG]::GetForegroundWindow())', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- const out = runPsHidden(scriptPath, { timeout: 4000 }).trim();+ const out = (await runPsHiddenPromise(scriptPath, { timeout: 4000 })).trim(); return out && out !== '0' ? out : null; } catch (e) { return null; } }@@ -2837,7 +2840,7 @@ async function pageSetLifecycleActive(page) { // Force a REAL activation of a window (AttachThreadInput + ALT-nudge + SetForegroundWindow) — the // user-input-equivalent activation that actually recomputes Chrome's occlusion, unlike a plain raise.-function forceActivateHwnd(hwnd) {+async function forceActivateHwnd(hwnd) { if (process.platform !== 'win32' || !hwnd) return false; try { const scriptPath = path.join(require('os').tmpdir(), `_adom_act_${process.pid}.ps1`);@@ -2857,7 +2860,7 @@ function forceActivateHwnd(hwnd) { '[void][WinAct]::BringWindowToTop($h); [void][WinAct]::SetForegroundWindow($h)', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- runPsHidden(scriptPath, { timeout: 4000 });+ await runPsHiddenPromise(scriptPath, { timeout: 4000 }); return true; } catch (e) { return false; } }@@ -2935,7 +2938,7 @@ async function unstickIfHiddenWhileFront(sessionId, session, fgHwnd, hwnd) { await new Promise(r => setTimeout(r, 700)); try { vis = await withTimeout(tab.page.evaluate('document.visibilityState'), 1500, 'vis-probe'); } catch (e) {} if (vis === 'hidden') {- forceActivateHwnd(hwnd);+ await forceActivateHwnd(hwnd); await new Promise(r => setTimeout(r, 900)); try { vis = await withTimeout(tab.page.evaluate('document.visibilityState'), 1500, 'vis-probe'); } catch (e) {} }@@ -6118,7 +6121,7 @@ async function reclaimUnmanagedWindows() { // window, respect the user: mark it foreground instead of parking. (async () => { try {- const fg = osGetForegroundWindow();+ const fg = await osGetForegroundWindow(); const hw = fg ? await cachedSessionHwnd(newId) : null; if (fg && hw && String(fg) === String(hw)) { ns._foreground = true; ns._userActiveAt = Date.now();@@ -6308,7 +6311,7 @@ async function badgePupTaskbar(sessionId) { // foreground window after, so callers (and tests) know for CERTAIN whether it worked. // cdpPort (the browser's CDP debug port) resolves the real browser via its LISTENING // process — reliable across the launcher→browser pid relaunch that Edge/Chrome do.-function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd, respectUser, targetHwnd) {+async function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd, respectUser, targetHwnd) { if (process.platform !== 'win32' || (!pid && !cdpPort)) return 'error'; try { const port = Number(cdpPort) || 0;@@ -6419,7 +6422,7 @@ function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd, respectUser, targetH 'if ($fg2 -eq $pup) { Write-Output "still-foreground" } else { Write-Output "backgrounded" }', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- const out = runPsHidden(scriptPath).trim();+ const out = (await runPsHiddenPromise(scriptPath, { timeout: 6000 })).trim(); return out || 'error'; } catch (e) { console.error(`[bg] pid ${pid} port ${cdpPort}: ${e.message}`); return 'error'; } }@@ -6564,7 +6567,7 @@ async function osBackgroundWatchdog(pid, cdpPort, restoreHwnd) { // respectUser (on the safety re-passes): if the window is NOT minimized and a real user input // happened in the last ~1.5s, the USER restored it → return "user-foreground", do NOT re-minimize. // Returns "minimized" | "user-foreground" | "still-foreground" | "no-window" | "error".-function osMinimizeToBackground(pid, cdpPort, respectUser) {+async function osMinimizeToBackground(pid, cdpPort, respectUser) { if (process.platform !== 'win32' || (!pid && !cdpPort)) return 'error'; try { const port = Number(cdpPort) || 0;@@ -6651,7 +6654,7 @@ function osMinimizeToBackground(pid, cdpPort, respectUser) { 'if ($fg2 -eq $pup) { Write-Output "still-foreground" } else { Write-Output "minimized" }', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- const out = runPsHidden(scriptPath).trim();+ const out = (await runPsHiddenPromise(scriptPath, { timeout: 6000 })).trim(); return out || 'error'; } catch (e) { console.error(`[min] pid ${pid} port ${cdpPort}: ${e.message}`); return 'error'; } }@@ -6660,7 +6663,7 @@ function osMinimizeToBackground(pid, cdpPort, respectUser) { // no direct API (CDP has no z-order; AD has no send-to-back verb yet — issue filed). Minimal // PS: one P/Invoke signature, no window-finding, no pid guessing, hwnd comes from AD's native // desktop_find_window. Everything else in the park is CDP/AD direct.-function osBottomUnlessForeground(hwnd, force) {+async function osBottomUnlessForeground(hwnd, force) { if (process.platform !== 'win32' || !hwnd) return 'error'; try { const scriptPath = path.join(require('os').tmpdir(), `_adom_zbot_${hwnd}.ps1`);@@ -6683,7 +6686,7 @@ function osBottomUnlessForeground(hwnd, force) { 'Write-Output "bottomed"', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- const out = runPsHidden(scriptPath, { timeout: 5000 }).trim();+ const out = (await runPsHiddenPromise(scriptPath, { timeout: 5000 })).trim(); return out || 'error'; } catch (e) { return 'error'; } }@@ -6965,7 +6968,7 @@ async function parkSessionWindowDirect(page, sessionId) { const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r); hwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null; } catch (e) {}- if (hwnd && osBottomUnlessForeground(hwnd, true) === 'bottomed') { bottomed = true; parkHwnd = hwnd; break; }+ if (hwnd && (await osBottomUnlessForeground(hwnd, true)) === 'bottomed') { bottomed = true; parkHwnd = hwnd; break; } } await new Promise(r2 => setTimeout(r2, 300)); }@@ -7044,7 +7047,7 @@ async function reassertBottomAfterNav(sessionId) { let bottomed = false; try { const hwnd = await cachedSessionHwnd(sessionId);- if (hwnd && osBottomUnlessForeground(hwnd, true) === 'bottomed') bottomed = true;+ if (hwnd && (await osBottomUnlessForeground(hwnd, true)) === 'bottomed') bottomed = true; } catch (e) {} if (bottomed) { console.log(`[park] "${sessionId}" (post-nav re-assert): bottomed`); return; } // v1.9.202 (John's log audit found `park-failed` on two windows and pup just moved on): a@@ -7053,7 +7056,7 @@ async function reassertBottomAfterNav(sessionId) { try { await new Promise(r => setTimeout(r, 1200)); const hwnd2 = await cachedSessionHwnd(sessionId);- if (hwnd2 && osBottomUnlessForeground(hwnd2, true) === 'bottomed') {+ if (hwnd2 && (await osBottomUnlessForeground(hwnd2, true)) === 'bottomed') { console.log(`[park] "${sessionId}" (post-nav re-assert, retry): bottomed`); return; }@@ -7098,7 +7101,7 @@ async function healOffscreenWindow(session, sessionId) { // at z-bottom, then hand focus straight back to whatever the user was in (AttachThreadInput). // Net effect: the flash clears, nothing pops, focus is never stolen. Returns "cleared" / // "cleared-still-foreground" / "already-foreground" (user already had it) / "no-window".-function osClearFlashByActivate(pid, cdpPort) {+async function osClearFlashByActivate(pid, cdpPort) { if (process.platform !== 'win32' || (!pid && !cdpPort)) return 'error'; try { const port = Number(cdpPort) || 0;@@ -7185,7 +7188,7 @@ function osClearFlashByActivate(pid, cdpPort) { 'if ($stillPup) { Write-Output "cleared-still-foreground" } else { Write-Output "cleared" }', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);- const out = runPsHidden(scriptPath).trim();+ const out = (await runPsHiddenPromise(scriptPath, { timeout: 6000 })).trim(); return out || 'error'; } catch (e) { console.error(`[clrflash] pid ${pid} port ${cdpPort}: ${e.message}`); return 'error'; } }@@ -7699,7 +7702,7 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer console.log(`Chrome launch attempt ${attemptNum} for profile "${profileName}" on CDP port ${debugPort}`); // v1.8.34: snapshot the user's foreground window right before launch (background only) // so we can hand keyboard focus back to it after the browser grabs it.- const priorForeground = foreground ? null : osGetForegroundWindow();+ const priorForeground = foreground ? null : await osGetForegroundWindow(); // v1.9.122 — REPARENT Chrome out of the bridge's process TREE (Windows). // Measured root cause of "every pup window dies on a bridge restart": Node's // { detached:true } gives a new process GROUP but the child stays a TREE child of@@ -7978,7 +7981,7 @@ async function createPageInNewWindow(browser, profileName) { // (invisible), so the user's typing is interrupted for at most ~1s and NOTHING ever appears on // their screen. This is the at-the-source fix; parks and re-asserts then act on an unfocused // window (osBottomUnlessForeground no longer mistakes the creation-grab for user intent).- const prevFg = osGetForegroundWindow();+ const prevFg = await osGetForegroundWindow(); await cdp.send('Target.createTarget', { url: 'about:blank', newWindow: true, left: birthLeft, top: -32000, width: 1280, height: 800 }); let fresh = null; for (let i = 0; i < 50 && !fresh; i++) {@@ -8003,7 +8006,7 @@ async function createPageInNewWindow(browser, profileName) { } catch (e) {} } try {- const nowFg = osGetForegroundWindow();+ const nowFg = await osGetForegroundWindow(); // Diagnostic only. The playbook (dev-skills/pup-bridge-dev, SOLVED PLAYBOOK) says this can // never show a steal: with --no-startup-window the browser is a background process and the OS // denies it the foreground. Measured TRUE on 2026-07-31. If this line EVER logs a steal, the@@ -12056,7 +12059,7 @@ const server = http.createServer(async (req, res) => { if (stop && process.platform === 'win32') { try { const br = browsers.get(session.profileName) && browsers.get(session.profileName).browser;- if (br && (br._detachedPid || br._cdpPort)) clearMethod = osClearFlashByActivate(br._detachedPid, br._cdpPort);+ if (br && (br._detachedPid || br._cdpPort)) clearMethod = await osClearFlashByActivate(br._detachedPid, br._cdpPort); } catch (e) {} try { _flashState.delete(session._sessionId || sessionId); } catch (e) {} }@@ -12094,7 +12097,7 @@ const server = http.createServer(async (req, res) => { ok = await macSetAppHiddenByPid(macSessionPid(session), true); } else { if (!session._hwnd) { try { await resolveSessionHwndByBounds(session, sessionId); } catch (e) {} }- ok = (be && be.browser) ? (osBackgroundWindowByPid(be.browser._detachedPid, be.browser._cdpPort, be.browser._priorForeground, false, session._hwnd) === 'backgrounded') : osSendWindowToBack(session.profileName);+ ok = (be && be.browser) ? ((await osBackgroundWindowByPid(be.browser._detachedPid, be.browser._cdpPort, be.browser._priorForeground, false, session._hwnd)) === 'backgrounded') : osSendWindowToBack(session.profileName); } session._foreground = false; session._fgWatchUntil = 0; // v1.9.260: a deliberate lower is not a "surfaced without pup" event - z-bottom keeps OS focus and the no-fault watch was mislabeling it@@ -12288,7 +12291,7 @@ const server = http.createServer(async (req, res) => { setTimeout(async () => { try { const s = sessions.get(sessionId);- const fg = osGetForegroundWindow();+ const fg = await osGetForegroundWindow(); const hw = s ? await cachedSessionHwnd(sessionId) : null; if (s && fg && hw) await unstickIfHiddenWhileFront(sessionId, s, fg, hw); } catch (e) {}@@ -14584,7 +14587,7 @@ const _onPupBound = async () => { const now = Date.now(); const watched = [...sessions].filter(([, s2]) => s2._fgWatchUntil && s2._fgWatchUntil > now && !s2._foreground && !s2._lostBrowser); if (!watched.length) return;- const fg = osGetForegroundWindow();+ const fg = await osGetForegroundWindow(); if (!fg) return; for (const [sid2, s2] of watched) { try {@@ -14604,7 +14607,7 @@ const _onPupBound = async () => { // A taskbar click is RECENT user input; a JS/Chrome raise has none. If input happened within // the last 1.5s, assume the USER raised it and stay SILENT. Errs toward silence (a JS raise // that coincides with the user typing elsewhere is also suppressed — the polite direction).- const _idle = osIdleMs();+ const _idle = await osIdleMs(); if (_idle != null && _idle < 1500) { console.log(`[fg-watch] "${sid2}" surfaced with recent user input (${_idle}ms idle) — assuming USER raised it, no caption`); break; } globalThis._lastNoFaultCaptionAt = Date.now(); console.log(`[fg-watch] "${sid2}" surfaced with no pup raise and no recent input (idle ${_idle}ms) — no-fault caption (2s, deduped)`);@@ -14701,7 +14704,7 @@ const _onPupBound = async () => { // v1.9.185 minimal-touch: no foreground polling, no unstick, no autonomous clicks. if (minimalTouch()) throw new Error('minimal-touch'); if (sessions.size > 0) {- const fg = osGetForegroundWindow();+ const fg = await osGetForegroundWindow(); if (fg) { for (const [sid, sess] of [...sessions]) { try {