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 with its AI thread's name in your taskbar or dock.
← Commit history
v1.8.17: hard background guarantee (sizing decoupled from foreground) + evaluateOnNewDocument title-suffix resilience; pkg 1.8.19
6 files changed
+53−22
SKILL.md+12−5@@ -162,11 +162,18 @@ adom-desktop browser_open_window '{"sessionId":"wiki","url":"<url>"}' ## Background by default — pup NEVER pops onto the user's screen unless asked -**Pup is the AI's workspace, not the user's.** `browser_open_window` opens **in the background by default**-(behind the user's windows, no focus stolen) — you do NOT need to lower it yourself; it's automatic. The-window is **fully drivable while hidden**: `browser_screenshot`, `browser_eval`, `browser_navigate` all-work on a background window. So for the overwhelmingly common case — the AI opens a page to screenshot/scrape/-debug — **just open it and drive it; the user never sees a thing.**+**Pup is the AI's workspace, not the user's.** `browser_open_window` ALWAYS opens **in the background**+(behind the user's windows, no focus stolen) — you do NOT need to lower it yourself; it's automatic and+guaranteed. The window is **fully drivable while hidden**: `browser_screenshot`, `browser_eval`,+`browser_navigate` all work on a background window. So for the overwhelmingly common case — the AI opens a+page to screenshot/scrape/debug — **just open it and drive it; the user never sees a thing.**++- **`foreground:true` is the ONLY thing that shows a window.** Nothing else foregrounds — not sizing, not+ navigating, not screenshotting. Pass it ONLY when the user explicitly wants to watch.+- **Sizing/positioning does NOT foreground** (v1.8.17+). `browser_open_window {width, height, x, y}` places+ the window for screenshot framing but keeps it in the background — so you can frame a window at an exact+ size for a clean screenshot **without ever disturbing the user's main work**. (Earlier versions+ foregrounded sized windows; that's fixed.) - **Only bring a window to the user's screen when they explicitly want to WATCH** — a demo, a recording, a "show me this" / "open it so I can see." Then (and only then) pass **`foreground:true`** on open, or call
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.16",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.16/adom-bridge-puppeteer-v1.8.16.zip",- "sha256": "8cd38ead0e20b054fad2893b731e43431b92e9cd2aec7ad0d3df4764a7c2d076",- "size": 142437,+ "version": "1.8.17",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.17/adom-bridge-puppeteer-v1.8.17.zip",+ "sha256": "3fc3755efa53248ad4f2cb77463aafa9bde5e6b8976a84cdab03ab0249c32936",+ "size": 143204, "verbPrefixes": [ "browser_", "desktop_recorder_",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-desktop-puppeteer-bridge",- "version": "1.8.18",+ "version": "1.8.19", "type": "app", "description": "CONTAINER-SIDE install \u2014 the pup skill pack in the wiki skillpack layout: the everyday `pup` user skill PLUS two bundled developer sub-skills (pup-bridge-dev, pup-bridge-publish; user-invocable:false, DEVELOPER-only descriptions so they never trigger for everyday users), all dropped into ~/.claude/skills/ AND ~/.codex/skills/ by an explicit install.sh list. It does NOT contain the bridge runtime: the bridge (Node + browser driving) lives on the DESKTOP \u2014 bundled inside Adom Desktop and published on the Releases tab as adom-bridge-puppeteer-v<ver>.zip, which Adom Desktop streams into its bridges-cache. TL;DR: pkg install = the skill pack; the bridge = the Release zip + bundled in Adom Desktop.", "keywords": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.16+1.8.17
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.16",+ "version": "1.8.17", "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.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+34−10@@ -157,6 +157,28 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user const tabId = nextTabId(session); const errors = []; setupErrorCollection(page, errors);+ // v1.8.17: PERSISTENT title-suffix injector. AD's browser_raise/lower_os_window+ // (AD-core verbs, not this bridge — they shadow our fallback handlers) locate the+ // OS window by its " (session: <id>)" title suffix. Post-goto page.evaluate re-adds+ // the suffix, but a NEW page sets its own title BEFORE that runs → a brief window+ // where the suffix is absent and AD's lookup fails ("No visible window with title+ // containing …") right after a navigation. evaluateOnNewDocument installs the+ // enforcer at document-start on EVERY navigation (before page scripts), so the+ // suffix reappears the instant the page sets its title — closing that gap.+ try {+ page.evaluateOnNewDocument((sid) => {+ const SUFFIX = ' (session: ' + sid + ')';+ const ensure = () => { try { if (document.title && !document.title.endsWith(SUFFIX)) document.title += SUFFIX; } catch (e) {} };+ const observe = () => {+ const t = document.querySelector('title') || document.head || document.documentElement;+ if (!t) return false;+ try { new MutationObserver(ensure).observe(t, { childList: true, subtree: true, characterData: true }); } catch (e) {}+ return true;+ };+ ensure();+ if (!observe()) document.addEventListener('DOMContentLoaded', () => { observe(); ensure(); });+ }, sessionId).catch(() => {});+ } catch (e) {} // Tag console logs with tabId so the caller can tell which tab emitted them page.on('console', msg => { try {@@ -2539,7 +2561,7 @@ const server = http.createServer(async (req, res) => { hint: m.hint || summary, related: m.related || [], pitfalls: m.pitfalls || [] }; }; sendJSON(res, { success: true, bridge: 'puppeteer', verbs: [- V('browser_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. Opens in the BACKGROUND by default (does not steal focus; still fully screenshottable/drivable) — pass foreground:true ONLY if the user wants to watch. Pass width+height (+ optional x,y, screen px) to open at a specific size/position (auto-foregrounds; default is maximized). profile defaults to sessionId. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'required str — task-prefix it (e.g. "mytask-web"); generic ids collide across threads', url: 'optional str', profile: 'optional str — defaults to sessionId', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — show it (default false = background)', width: 'optional int px', height: 'optional int px', x: 'optional int px', y: 'optional int px' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }),+ V('browser_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. ALWAYS opens in the BACKGROUND (does not steal focus; never disturbs the user\'s main work; still fully screenshottable/drivable) UNLESS you pass foreground:true — do that ONLY when the user explicitly wants to watch. Pass width+height (+ optional x,y, screen px) to place it at a specific size/position for screenshot framing — this NO LONGER foregrounds it (sizing stays in the background too; default unsized is maximized). profile defaults to sessionId. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'required str — task-prefix it (e.g. "mytask-web"); generic ids collide across threads', url: 'optional str', profile: 'optional str — defaults to sessionId', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — the ONLY way to show the window (default false = background, always, even when sized)', width: 'optional int px — sizes for screenshot framing, stays background', height: 'optional int px', x: 'optional int px', y: 'optional int px' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }), V('browser_open_tab', 'Open a new tab in a session.', { sessionId: 'required str', url: 'optional str' }, 30, { sessionId: 'work', url: 'https://example.com' }), V('browser_close_tab', 'Close a tab.', { sessionId: 'required str', tabId: 'optional str (default active)' }, 15, { sessionId: 'work' }), V('browser_close_window', 'Close a window/session (its tabs go too).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),@@ -2687,22 +2709,24 @@ const server = http.createServer(async (req, res) => { const info = await getSessionInfo(sessionId, session); // v1.8.0: BACKGROUND BY DEFAULT. pup is the AI's workspace, not the user's — // a window that pops to the foreground and covers what the user is doing is- // disruptive. So unless the caller EXPLICITLY asks to show it (foreground/raise/- // show:true — e.g. the user said "let me watch" or it's a recording/demo), we- // push it behind the user's windows immediately. It stays fully drivable +- // screenshottable there (occluded, not minimized).- // Optional explicit window size/position (screen px). A sized/positioned window- // is one the user wants to SEE, so it's foregrounded (not sent to the back).+ // disruptive. v1.8.17: background is now the HARD default — a window is+ // foregrounded ONLY when the caller EXPLICITLY asks (foreground/raise/show:true,+ // e.g. the user said "let me watch" / a recording). Sizing/positioning NO LONGER+ // foregrounds: a sized window is placed via CDP (works fine while occluded) and+ // STILL pushed to the back, so screenshot-framing never disturbs the user. The+ // window stays fully drivable + screenshottable in the background (occluded, not+ // minimized). Order matters: size FIRST, then send to back, so the final z-order+ // is background even after the bounds call. const w = Number(args.width), h = Number(args.height), wx = Number(args.x), wy = Number(args.y); const sized = Number.isFinite(w) && Number.isFinite(h);- const foreground = !!(args.foreground || args.raise || args.show) || sized;+ const foreground = !!(args.foreground || args.raise || args.show); if (sized) { try { await setSessionWindowBounds(session, { x: wx, y: wy, width: w, height: h }); } catch (e) {} } if (!foreground) { try { osSendWindowToBack(args.profile); } catch (e) {} } // Verbose browser report: which browser we actually drove, what else is available. const brep = browserPickReport(); const bgHint = foreground- ? 'Opened in the FOREGROUND as requested. Return it to the background with browser_lower_os_window once the user is done watching.'- : 'Opened in the BACKGROUND (behind the user\'s windows, did NOT steal focus) — this is the default so pup never disrupts the user. It is FULLY drivable there: browser_screenshot / browser_eval / browser_navigate all work while it stays hidden. ONLY bring it to the user\'s screen if they explicitly want to WATCH (a demo, a recording, a "show me") — then call browser_raise_os_window (or pass foreground:true on open). Otherwise keep it in the background.';+ ? 'Opened in the FOREGROUND as EXPLICITLY requested (foreground/raise/show:true). Return it to the background with browser_lower_os_window once the user is done watching.'+ : `Opened in the BACKGROUND (behind the user's windows, did NOT steal focus)${sized ? ' — and placed at the requested size/position WITHOUT foregrounding, so framing it for a screenshot never disturbs the user' : ''}. This is the HARD default so pup never disrupts the user's main work. It is FULLY drivable here: browser_screenshot / browser_eval / browser_navigate all work while it stays hidden. ONLY bring it to the user's screen if they EXPLICITLY want to WATCH (a demo, a recording, a "show me") — pass foreground:true on open (or call browser_raise_os_window). Sizing alone does NOT foreground anymore.`; // v1.8.16: ownership/reuse verdict — reprimand grabs, bless refreshes. let ownHint = ''; if (reuse && reuse.navigatedAway) {